@modelprofile.com/flexharness-providers 7.0.1 → 8.1.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_ts_openai_account/smartai.providers.interfaces.d.ts +20 -2
- package/dist_ts_openai_account/smartai.providers.openai.d.ts +3 -3
- package/dist_ts_openai_account/smartai.providers.openai.js +61 -17
- package/dist_ts_openai_auth/index.d.ts +1 -3
- package/dist_ts_openai_auth/index.js +2 -7
- package/dist_ts_openai_auth/interfaces.d.ts +28 -0
- package/dist_ts_openai_auth/smartai.auth.openai.d.ts +5 -1
- package/dist_ts_openai_auth/smartai.auth.openai.js +66 -12
- package/dist_ts_openai_auth/smartai.connection.openai.d.ts +4 -0
- package/dist_ts_openai_auth/smartai.connection.openai.js +98 -0
- package/package.json +2 -2
- package/readme.md +5 -1
- package/ts_openai_account/readme.md +52 -0
- package/ts_openai_account/smartai.providers.interfaces.ts +25 -2
- package/ts_openai_account/smartai.providers.openai.ts +73 -17
- package/ts_openai_auth/index.ts +1 -9
- package/ts_openai_auth/interfaces.ts +35 -1
- package/ts_openai_auth/readme.md +20 -0
- package/ts_openai_auth/smartai.auth.openai.ts +82 -12
- package/ts_openai_auth/smartai.connection.openai.ts +143 -0
- package/ts_providers/openai/readme.md +5 -0
- package/ts_providers/readme.md +5 -1
|
@@ -3,6 +3,7 @@ import * as plugins from './plugins.js';
|
|
|
3
3
|
import {
|
|
4
4
|
OPENAI_CHATGPT_CODEX_BASE_URL,
|
|
5
5
|
OPENAI_CHATGPT_DEFAULT_ORIGINATOR,
|
|
6
|
+
OpenAiChatGptAuthError,
|
|
6
7
|
completeOpenAiChatGptDeviceCodeLogin,
|
|
7
8
|
createOpenAiChatGptBrowserAuthorization,
|
|
8
9
|
exchangeOpenAiChatGptBrowserAuthorizationCode,
|
|
@@ -29,6 +30,7 @@ import type {
|
|
|
29
30
|
ISmartAiProviderOperationOptions,
|
|
30
31
|
ISmartAiProviderRateLimitDetails,
|
|
31
32
|
ISmartAiProviderRateLimitWindow,
|
|
33
|
+
TOpenAiChatGptReadCredential,
|
|
32
34
|
TSmartAiProviderCancelStatus,
|
|
33
35
|
TSmartAiProviderAccountRateLimitsResult,
|
|
34
36
|
TSmartAiProviderCredential,
|
|
@@ -183,6 +185,46 @@ const toTokenData = (credentialArg: IOpenAiChatGptOAuthCredential): IOpenAiChatG
|
|
|
183
185
|
};
|
|
184
186
|
};
|
|
185
187
|
|
|
188
|
+
interface IOpenAiChatGptReadAccess {
|
|
189
|
+
accessToken: string;
|
|
190
|
+
accountId?: string;
|
|
191
|
+
isFedrampAccount: boolean;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const toReadAccess = (
|
|
195
|
+
credentialArg: TOpenAiChatGptReadCredential,
|
|
196
|
+
): IOpenAiChatGptReadAccess => {
|
|
197
|
+
if (!isRecord(credentialArg)) throw createSmartAiProviderError('CREDENTIAL_INVALID');
|
|
198
|
+
if (credentialArg.kind === 'chatgptOAuth') {
|
|
199
|
+
const tokenData = toTokenData(credentialArg);
|
|
200
|
+
return {
|
|
201
|
+
accessToken: tokenData.accessToken,
|
|
202
|
+
accountId: tokenData.tokenInfo.chatgptAccountId,
|
|
203
|
+
isFedrampAccount: tokenData.tokenInfo.chatgptAccountIsFedramp,
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
const access = credentialArg;
|
|
207
|
+
if (
|
|
208
|
+
access.kind !== 'chatgptAccess'
|
|
209
|
+
|| access.providerId !== 'openai'
|
|
210
|
+
|| typeof access.accessToken !== 'string'
|
|
211
|
+
|| access.accessToken.length === 0
|
|
212
|
+
|| plugins.Buffer.byteLength(access.accessToken, 'utf8') > maximumHeaderBytes
|
|
213
|
+
|| typeof access.accountId !== 'string'
|
|
214
|
+
|| access.accountId.length === 0
|
|
215
|
+
|| plugins.Buffer.byteLength(access.accountId, 'utf8') > maximumHeaderBytes
|
|
216
|
+
|| typeof access.isFedrampAccount !== 'boolean'
|
|
217
|
+
|| Object.keys(access).length !== 5
|
|
218
|
+
) {
|
|
219
|
+
throw createSmartAiProviderError('CREDENTIAL_INVALID');
|
|
220
|
+
}
|
|
221
|
+
return {
|
|
222
|
+
accessToken: access.accessToken,
|
|
223
|
+
accountId: access.accountId,
|
|
224
|
+
isFedrampAccount: access.isFedrampAccount,
|
|
225
|
+
};
|
|
226
|
+
};
|
|
227
|
+
|
|
186
228
|
const toCredential = (
|
|
187
229
|
tokenDataArg: IOpenAiChatGptTokenData,
|
|
188
230
|
lastRefreshAtArg?: string,
|
|
@@ -849,6 +891,7 @@ export class OpenAiProviderAdapter implements ISmartAiProviderAdapter {
|
|
|
849
891
|
success: false,
|
|
850
892
|
credential: credentialArg,
|
|
851
893
|
errorCode: waitOperation.timedOut ? 'TIMEOUT' : 'ABORTED',
|
|
894
|
+
requestOutcome: 'notSent',
|
|
852
895
|
};
|
|
853
896
|
waitOperation.cleanup();
|
|
854
897
|
return failure;
|
|
@@ -888,6 +931,7 @@ export class OpenAiProviderAdapter implements ISmartAiProviderAdapter {
|
|
|
888
931
|
success: false,
|
|
889
932
|
credential: credentialArg,
|
|
890
933
|
errorCode: waitOperation.timedOut ? 'TIMEOUT' : 'ABORTED',
|
|
934
|
+
requestOutcome: 'outcomeUnknown',
|
|
891
935
|
});
|
|
892
936
|
waitOperation.signal.addEventListener('abort', onAbort, { once: true });
|
|
893
937
|
});
|
|
@@ -914,7 +958,12 @@ export class OpenAiProviderAdapter implements ISmartAiProviderAdapter {
|
|
|
914
958
|
const tokenChanged = refreshed.accessToken !== input.accessToken
|
|
915
959
|
|| refreshed.refreshToken !== input.refreshToken;
|
|
916
960
|
if (!tokenChanged || !refreshed.idToken) {
|
|
917
|
-
return {
|
|
961
|
+
return {
|
|
962
|
+
success: false,
|
|
963
|
+
credential: credentialArg,
|
|
964
|
+
errorCode: 'REFRESH_FAILED',
|
|
965
|
+
requestOutcome: 'outcomeUnknown',
|
|
966
|
+
};
|
|
918
967
|
}
|
|
919
968
|
const previous = credentialArg.lastRefreshAt === undefined
|
|
920
969
|
? 0
|
|
@@ -929,6 +978,15 @@ export class OpenAiProviderAdapter implements ISmartAiProviderAdapter {
|
|
|
929
978
|
success: false,
|
|
930
979
|
credential: credentialArg,
|
|
931
980
|
errorCode: classifyOperationError(errorArg, operation, 'REFRESH_FAILED'),
|
|
981
|
+
requestOutcome: errorArg instanceof OpenAiChatGptAuthError
|
|
982
|
+
? errorArg.requestOutcome ?? 'outcomeUnknown'
|
|
983
|
+
: 'outcomeUnknown',
|
|
984
|
+
...(errorArg instanceof OpenAiChatGptAuthError && errorArg.status !== undefined
|
|
985
|
+
? { responseStatus: errorArg.status }
|
|
986
|
+
: {}),
|
|
987
|
+
...(errorArg instanceof OpenAiChatGptAuthError && errorArg.retryAfterMs !== undefined
|
|
988
|
+
? { retryAfterMs: errorArg.retryAfterMs }
|
|
989
|
+
: {}),
|
|
932
990
|
};
|
|
933
991
|
} finally {
|
|
934
992
|
this.operations.delete(operation.controller);
|
|
@@ -937,11 +995,10 @@ export class OpenAiProviderAdapter implements ISmartAiProviderAdapter {
|
|
|
937
995
|
}
|
|
938
996
|
|
|
939
997
|
private async fetchCatalog(
|
|
940
|
-
|
|
998
|
+
accessArg: IOpenAiChatGptReadAccess,
|
|
941
999
|
signalArg: AbortSignal,
|
|
942
1000
|
): Promise<ISmartAiProviderModel[]> {
|
|
943
|
-
const
|
|
944
|
-
const accountId = tokenData.tokenInfo.chatgptAccountId;
|
|
1001
|
+
const accountId = accessArg.accountId;
|
|
945
1002
|
const url = `${OPENAI_CHATGPT_CODEX_BASE_URL}/models?client_version=${encodeURIComponent(defaultClientVersion)}`;
|
|
946
1003
|
let response: Response;
|
|
947
1004
|
let onAbort!: () => void;
|
|
@@ -952,9 +1009,9 @@ export class OpenAiProviderAdapter implements ISmartAiProviderAdapter {
|
|
|
952
1009
|
signal: signalArg,
|
|
953
1010
|
headers: {
|
|
954
1011
|
Accept: 'application/json',
|
|
955
|
-
Authorization: `Bearer ${
|
|
1012
|
+
Authorization: `Bearer ${accessArg.accessToken}`,
|
|
956
1013
|
...(accountId ? { 'ChatGPT-Account-ID': accountId } : {}),
|
|
957
|
-
...(
|
|
1014
|
+
...(accessArg.isFedrampAccount ? { 'X-OpenAI-Fedramp': 'true' } : {}),
|
|
958
1015
|
originator: this.originator,
|
|
959
1016
|
version: defaultClientVersion,
|
|
960
1017
|
},
|
|
@@ -1023,18 +1080,18 @@ export class OpenAiProviderAdapter implements ISmartAiProviderAdapter {
|
|
|
1023
1080
|
}
|
|
1024
1081
|
|
|
1025
1082
|
public async listModels(
|
|
1026
|
-
credentialArg:
|
|
1083
|
+
credentialArg: TOpenAiChatGptReadCredential,
|
|
1027
1084
|
optionsArg: ISmartAiProviderModelListOptions = {},
|
|
1028
1085
|
): Promise<TSmartAiProviderModelListResult> {
|
|
1029
1086
|
this.requireOpen();
|
|
1030
|
-
|
|
1087
|
+
const access = toReadAccess(credentialArg);
|
|
1031
1088
|
const options = normalizeModelListOptions(optionsArg);
|
|
1032
1089
|
const operation = createOperation(options, defaultOperationTimeoutMs);
|
|
1033
1090
|
this.operations.add(operation.controller);
|
|
1034
1091
|
try {
|
|
1035
1092
|
let catalog: ISmartAiProviderModel[];
|
|
1036
1093
|
try {
|
|
1037
|
-
catalog = await this.fetchCatalog(
|
|
1094
|
+
catalog = await this.fetchCatalog(access, operation.signal);
|
|
1038
1095
|
} catch (errorArg) {
|
|
1039
1096
|
if (!(errorArg instanceof ModelHttpError) || errorArg.status !== 401) throw errorArg;
|
|
1040
1097
|
return { success: false, errorCode: 'CREDENTIAL_REFRESH_REQUIRED' };
|
|
@@ -1084,11 +1141,10 @@ export class OpenAiProviderAdapter implements ISmartAiProviderAdapter {
|
|
|
1084
1141
|
}
|
|
1085
1142
|
|
|
1086
1143
|
private async fetchAccountRateLimits(
|
|
1087
|
-
|
|
1144
|
+
accessArg: IOpenAiChatGptReadAccess,
|
|
1088
1145
|
signalArg: AbortSignal,
|
|
1089
1146
|
): Promise<ISmartAiProviderAccountRateLimits> {
|
|
1090
|
-
const
|
|
1091
|
-
const accountId = tokenData.tokenInfo.chatgptAccountId;
|
|
1147
|
+
const accountId = accessArg.accountId;
|
|
1092
1148
|
let response: Response;
|
|
1093
1149
|
let onAbort!: () => void;
|
|
1094
1150
|
try {
|
|
@@ -1098,9 +1154,9 @@ export class OpenAiProviderAdapter implements ISmartAiProviderAdapter {
|
|
|
1098
1154
|
signal: signalArg,
|
|
1099
1155
|
headers: {
|
|
1100
1156
|
Accept: 'application/json',
|
|
1101
|
-
Authorization: `Bearer ${
|
|
1157
|
+
Authorization: `Bearer ${accessArg.accessToken}`,
|
|
1102
1158
|
...(accountId ? { 'ChatGPT-Account-Id': accountId } : {}),
|
|
1103
|
-
...(
|
|
1159
|
+
...(accessArg.isFedrampAccount ? { 'X-OpenAI-Fedramp': 'true' } : {}),
|
|
1104
1160
|
'User-Agent': this.originator,
|
|
1105
1161
|
},
|
|
1106
1162
|
});
|
|
@@ -1130,16 +1186,16 @@ export class OpenAiProviderAdapter implements ISmartAiProviderAdapter {
|
|
|
1130
1186
|
}
|
|
1131
1187
|
|
|
1132
1188
|
public async getAccountRateLimits(
|
|
1133
|
-
credentialArg:
|
|
1189
|
+
credentialArg: TOpenAiChatGptReadCredential,
|
|
1134
1190
|
optionsArg: ISmartAiProviderOperationOptions = {},
|
|
1135
1191
|
): Promise<TSmartAiProviderAccountRateLimitsResult> {
|
|
1136
1192
|
this.requireOpen();
|
|
1137
|
-
|
|
1193
|
+
const access = toReadAccess(credentialArg);
|
|
1138
1194
|
const operation = createOperation(optionsArg, defaultOperationTimeoutMs);
|
|
1139
1195
|
this.operations.add(operation.controller);
|
|
1140
1196
|
try {
|
|
1141
1197
|
try {
|
|
1142
|
-
const rateLimits = await this.fetchAccountRateLimits(
|
|
1198
|
+
const rateLimits = await this.fetchAccountRateLimits(access, operation.signal);
|
|
1143
1199
|
return { success: true, rateLimits };
|
|
1144
1200
|
} catch (errorArg) {
|
|
1145
1201
|
if (!(errorArg instanceof AccountRateLimitsHttpError) || errorArg.status !== 401) {
|
package/ts_openai_auth/index.ts
CHANGED
|
@@ -1,12 +1,4 @@
|
|
|
1
|
-
import type { IOpenAiModelConnection } from '@modelprofile.com/flexharness-providers/openai';
|
|
2
|
-
import type { IOpenAiChatGptAuthCredentials } from './interfaces.js';
|
|
3
|
-
import { createOpenAiChatGptProviderSettings } from './smartai.auth.openai.js';
|
|
4
|
-
import { createOpenAiChatGptInstructionsMiddleware } from './smartai.middleware.openai.js';
|
|
5
1
|
export * from './interfaces.js';
|
|
6
2
|
export * from './smartai.auth.openai.js';
|
|
3
|
+
export * from './smartai.connection.openai.js';
|
|
7
4
|
export * from './smartai.middleware.openai.js';
|
|
8
|
-
|
|
9
|
-
export const createOpenAiChatGptModelConnection = (credentials: IOpenAiChatGptAuthCredentials): IOpenAiModelConnection => ({
|
|
10
|
-
settings: createOpenAiChatGptProviderSettings(credentials),
|
|
11
|
-
middleware: createOpenAiChatGptInstructionsMiddleware(),
|
|
12
|
-
});
|
|
@@ -13,10 +13,42 @@ export interface IOpenAiChatGptAuthCredentials {
|
|
|
13
13
|
refreshToken?: string;
|
|
14
14
|
idToken?: string;
|
|
15
15
|
accountId?: string;
|
|
16
|
+
/** Verified from the current access token when tokenInfo is not supplied. */
|
|
17
|
+
isFedrampAccount?: boolean;
|
|
16
18
|
tokenInfo?: IOpenAiChatGptTokenInfo;
|
|
17
19
|
originator?: string;
|
|
18
20
|
}
|
|
19
21
|
|
|
22
|
+
export interface IOpenAiChatGptAccessIdentity {
|
|
23
|
+
/** Exact workspace claim expected on every resolved access token. */
|
|
24
|
+
readonly accountId: string | undefined;
|
|
25
|
+
/** Exact residency claim expected on every resolved access token. */
|
|
26
|
+
readonly isFedrampAccount: boolean;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface IOpenAiChatGptAccessRequest {
|
|
30
|
+
/** The provider request signal. A resolver should pass it through to its authority. */
|
|
31
|
+
readonly signal: AbortSignal;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Access-only result. Refresh and ID tokens are not part of the request-time contract. */
|
|
35
|
+
export interface IOpenAiChatGptResolvedAccess {
|
|
36
|
+
readonly accessToken: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface IOpenAiChatGptDynamicModelConnectionOptions {
|
|
40
|
+
/** Pins the account and residency boundary for the lifetime of the connection. */
|
|
41
|
+
readonly expectedIdentity: IOpenAiChatGptAccessIdentity;
|
|
42
|
+
/** Stable provider originator header. Defaults to `smartai`. */
|
|
43
|
+
readonly originator?: string;
|
|
44
|
+
/** Resolves current access immediately before every provider network request. */
|
|
45
|
+
readonly resolveAccess: (
|
|
46
|
+
request: IOpenAiChatGptAccessRequest,
|
|
47
|
+
) => Promise<IOpenAiChatGptResolvedAccess>;
|
|
48
|
+
/** Optional underlying transport. Defaults to the current global fetch. */
|
|
49
|
+
readonly fetch?: typeof fetch;
|
|
50
|
+
}
|
|
51
|
+
|
|
20
52
|
export interface IOpenAiChatGptTokenData extends IOpenAiChatGptAuthCredentials {
|
|
21
53
|
refreshToken: string;
|
|
22
54
|
tokenInfo: IOpenAiChatGptTokenInfo;
|
|
@@ -37,6 +69,9 @@ export interface IOpenAiChatGptAuthOptions {
|
|
|
37
69
|
safeErrors?: boolean;
|
|
38
70
|
}
|
|
39
71
|
|
|
72
|
+
/** A failed refresh is replayable only when no request was handed to fetch. */
|
|
73
|
+
export type TOpenAiChatGptRequestOutcome = 'notSent' | 'outcomeUnknown';
|
|
74
|
+
|
|
40
75
|
export interface IOpenAiChatGptBrowserAuthorizationOptions extends IOpenAiChatGptAuthOptions {
|
|
41
76
|
redirectUri: string;
|
|
42
77
|
originator?: string;
|
|
@@ -86,4 +121,3 @@ export interface IOpenAiChatGptDeviceCodePollOptions extends IOpenAiChatGptAuthO
|
|
|
86
121
|
export interface IOpenAiChatGptCompleteDeviceCodeOptions extends IOpenAiChatGptDeviceCodePollOptions {
|
|
87
122
|
forcedChatGptWorkspaceId?: string;
|
|
88
123
|
}
|
|
89
|
-
|
package/ts_openai_auth/readme.md
CHANGED
|
@@ -11,6 +11,26 @@ pnpm add @modelprofile.com/flexharness-providers
|
|
|
11
11
|
See the [toolbox usage and migration guide](https://code.foss.global/modelprofile.com/flexharness#readme)
|
|
12
12
|
for composition examples and the SmartAI/SmartAgent migration map.
|
|
13
13
|
|
|
14
|
+
The refresh helper preserves safe request-outcome metadata on
|
|
15
|
+
`OpenAiChatGptAuthError`: `requestOutcome` is `notSent` only before the HTTP
|
|
16
|
+
request is handed to `fetch`, and `outcomeUnknown` afterward. An observed
|
|
17
|
+
response can also supply `status` and a bounded `retryAfterMs`. A 429 response
|
|
18
|
+
does not prove that a rotating grant remained usable. The default safe-error
|
|
19
|
+
mode omits provider response bodies.
|
|
20
|
+
|
|
21
|
+
`createOpenAiChatGptModelConnection()` derives account and FedRAMP headers from
|
|
22
|
+
the current access token. The token must contain an unexpired `exp` claim;
|
|
23
|
+
supplied `accountId`, `isFedrampAccount`, and `tokenInfo` must agree with its
|
|
24
|
+
claims. A saved ID-token snapshot cannot override current access-token facts.
|
|
25
|
+
|
|
26
|
+
Use `createOpenAiChatGptDynamicModelConnection()` for delegated access owned by
|
|
27
|
+
an external authority. Pin its exact account and FedRAMP identity, then provide
|
|
28
|
+
an async `resolveAccess({ signal })` callback. The connection resolves and
|
|
29
|
+
validates current access immediately before every provider HTTP request, so
|
|
30
|
+
multi-step tool runs do not retain the token that admitted the run. Resolution,
|
|
31
|
+
identity, expiry, endpoint, redirect, and abort failures stop before the
|
|
32
|
+
underlying transport is called.
|
|
33
|
+
|
|
14
34
|
## Issue Reporting and Security
|
|
15
35
|
|
|
16
36
|
Report issues and security concerns through [community.foss.global](https://community.foss.global).
|
|
@@ -13,6 +13,7 @@ import type {
|
|
|
13
13
|
IOpenAiChatGptTokenData,
|
|
14
14
|
IOpenAiChatGptTokenInfo,
|
|
15
15
|
TOpenAiChatGptBrowserCallback,
|
|
16
|
+
TOpenAiChatGptRequestOutcome,
|
|
16
17
|
} from './interfaces.js';
|
|
17
18
|
|
|
18
19
|
export const OPENAI_CHATGPT_AUTH_ISSUER = 'https://auth.openai.com';
|
|
@@ -43,12 +44,21 @@ const utf8ByteLength = (valueArg: string): number => textEncoder.encode(valueArg
|
|
|
43
44
|
export class OpenAiChatGptAuthError extends Error {
|
|
44
45
|
public status?: number;
|
|
45
46
|
public body?: string;
|
|
46
|
-
|
|
47
|
-
|
|
47
|
+
public requestOutcome?: TOpenAiChatGptRequestOutcome;
|
|
48
|
+
public retryAfterMs?: number;
|
|
49
|
+
|
|
50
|
+
constructor(message: string, options: {
|
|
51
|
+
status?: number;
|
|
52
|
+
body?: string;
|
|
53
|
+
requestOutcome?: TOpenAiChatGptRequestOutcome;
|
|
54
|
+
retryAfterMs?: number;
|
|
55
|
+
} = {}) {
|
|
48
56
|
super(message);
|
|
49
57
|
this.name = 'OpenAiChatGptAuthError';
|
|
50
58
|
this.status = options.status;
|
|
51
59
|
this.body = options.body;
|
|
60
|
+
this.requestOutcome = options.requestOutcome;
|
|
61
|
+
this.retryAfterMs = options.retryAfterMs;
|
|
52
62
|
}
|
|
53
63
|
}
|
|
54
64
|
|
|
@@ -215,6 +225,17 @@ async function readJson(
|
|
|
215
225
|
}
|
|
216
226
|
}
|
|
217
227
|
|
|
228
|
+
const parseRetryAfterMs = (headerArg: string | null): number | undefined => {
|
|
229
|
+
if (!headerArg || headerArg.length > 128) return undefined;
|
|
230
|
+
const maximumDelayMs = 24 * 60 * 60 * 1000;
|
|
231
|
+
if (/^\d{1,10}$/.test(headerArg)) {
|
|
232
|
+
return Math.min(Number(headerArg) * 1000, maximumDelayMs);
|
|
233
|
+
}
|
|
234
|
+
const retryAt = Date.parse(headerArg);
|
|
235
|
+
if (!Number.isFinite(retryAt)) return undefined;
|
|
236
|
+
return Math.min(Math.max(0, retryAt - Date.now()), maximumDelayMs);
|
|
237
|
+
};
|
|
238
|
+
|
|
218
239
|
async function runFetch<TResult>(
|
|
219
240
|
url: string,
|
|
220
241
|
init: RequestInit,
|
|
@@ -225,9 +246,14 @@ async function runFetch<TResult>(
|
|
|
225
246
|
) => Promise<TResult>,
|
|
226
247
|
timeoutMsArg = AUTH_REQUEST_TIMEOUT_MS,
|
|
227
248
|
): Promise<TResult> {
|
|
228
|
-
|
|
249
|
+
if (options.signal?.aborted) {
|
|
250
|
+
throw new OpenAiChatGptAuthError('OpenAI ChatGPT authentication was aborted.', {
|
|
251
|
+
requestOutcome: 'notSent',
|
|
252
|
+
});
|
|
253
|
+
}
|
|
229
254
|
const controller = new AbortController();
|
|
230
255
|
let response: Response | undefined;
|
|
256
|
+
let requestStarted = false;
|
|
231
257
|
const onExternalAbort = (): void => controller.abort();
|
|
232
258
|
const onRequestAbort = (): void => {
|
|
233
259
|
void response?.body?.cancel().catch(() => undefined);
|
|
@@ -245,7 +271,9 @@ async function runFetch<TResult>(
|
|
|
245
271
|
try {
|
|
246
272
|
return await Promise.race([
|
|
247
273
|
(async () => {
|
|
248
|
-
|
|
274
|
+
const fetchImplementation = getFetch(options);
|
|
275
|
+
requestStarted = true;
|
|
276
|
+
response = await fetchImplementation(url, {
|
|
249
277
|
...init,
|
|
250
278
|
redirect: 'error',
|
|
251
279
|
signal: controller.signal,
|
|
@@ -259,13 +287,29 @@ async function runFetch<TResult>(
|
|
|
259
287
|
aborted,
|
|
260
288
|
]);
|
|
261
289
|
} catch (error) {
|
|
262
|
-
|
|
290
|
+
const requestOutcome: TOpenAiChatGptRequestOutcome = requestStarted
|
|
291
|
+
? 'outcomeUnknown'
|
|
292
|
+
: 'notSent';
|
|
293
|
+
const retryAfterMs = response ? parseRetryAfterMs(response.headers.get('retry-after')) : undefined;
|
|
294
|
+
if (error instanceof OpenAiChatGptAuthError) {
|
|
295
|
+
throw new OpenAiChatGptAuthError(error.message, {
|
|
296
|
+
...(error.status !== undefined ? { status: error.status }
|
|
297
|
+
: response ? { status: response.status } : {}),
|
|
298
|
+
...(error.body !== undefined ? { body: error.body } : {}),
|
|
299
|
+
requestOutcome,
|
|
300
|
+
...(retryAfterMs !== undefined ? { retryAfterMs } : {}),
|
|
301
|
+
});
|
|
302
|
+
}
|
|
263
303
|
if (!usesSafeErrors(options)) throw error;
|
|
264
|
-
throw
|
|
304
|
+
throw new OpenAiChatGptAuthError(
|
|
265
305
|
options.signal?.aborted
|
|
266
306
|
? 'OpenAI ChatGPT authentication was aborted.'
|
|
267
307
|
: 'OpenAI ChatGPT authentication request failed.',
|
|
268
|
-
|
|
308
|
+
{
|
|
309
|
+
requestOutcome,
|
|
310
|
+
...(response ? { status: response.status } : {}),
|
|
311
|
+
...(retryAfterMs !== undefined ? { retryAfterMs } : {}),
|
|
312
|
+
},
|
|
269
313
|
);
|
|
270
314
|
} finally {
|
|
271
315
|
clearTimeout(timer);
|
|
@@ -370,7 +414,9 @@ export function parseOpenAiChatGptTokenInfo(token: string): IOpenAiChatGptTokenI
|
|
|
370
414
|
chatgptUserId: asOptionalString(auth?.chatgpt_user_id) ?? asOptionalString(auth?.user_id),
|
|
371
415
|
chatgptAccountId: asOptionalString(auth?.chatgpt_account_id),
|
|
372
416
|
chatgptAccountIsFedramp: auth?.chatgpt_account_is_fedramp === true,
|
|
373
|
-
expiresAt: expiresAtSeconds
|
|
417
|
+
expiresAt: expiresAtSeconds !== undefined
|
|
418
|
+
? new Date(expiresAtSeconds * 1000).toISOString()
|
|
419
|
+
: undefined,
|
|
374
420
|
rawJwt: token,
|
|
375
421
|
};
|
|
376
422
|
}
|
|
@@ -760,16 +806,40 @@ export function createOpenAiChatGptProviderSettings(credentials: IOpenAiChatGptA
|
|
|
760
806
|
baseURL: string;
|
|
761
807
|
headers: Record<string, string>;
|
|
762
808
|
} {
|
|
763
|
-
const
|
|
809
|
+
const currentTokenInfo = parseOpenAiChatGptTokenInfo(credentials.accessToken);
|
|
810
|
+
if (
|
|
811
|
+
currentTokenInfo.expiresAt === undefined
|
|
812
|
+
|| Date.parse(currentTokenInfo.expiresAt) <= Date.now()
|
|
813
|
+
) {
|
|
814
|
+
throw new OpenAiChatGptAuthError('OpenAI ChatGPT access token has no valid future expiry.');
|
|
815
|
+
}
|
|
816
|
+
const tokenAccountId = currentTokenInfo.chatgptAccountId;
|
|
764
817
|
if (
|
|
765
818
|
credentials.accountId !== undefined
|
|
766
|
-
&& tokenAccountId !== undefined
|
|
767
819
|
&& credentials.accountId !== tokenAccountId
|
|
768
820
|
) {
|
|
769
821
|
throw new OpenAiChatGptAuthError('OpenAI ChatGPT account ID does not match the token claim.');
|
|
770
822
|
}
|
|
771
|
-
|
|
772
|
-
|
|
823
|
+
if (
|
|
824
|
+
credentials.tokenInfo !== undefined
|
|
825
|
+
&& (
|
|
826
|
+
credentials.tokenInfo.chatgptAccountId !== tokenAccountId
|
|
827
|
+
|| credentials.tokenInfo.chatgptAccountIsFedramp !== currentTokenInfo.chatgptAccountIsFedramp
|
|
828
|
+
)
|
|
829
|
+
) {
|
|
830
|
+
throw new OpenAiChatGptAuthError('OpenAI ChatGPT token information does not match the current access token.');
|
|
831
|
+
}
|
|
832
|
+
if (
|
|
833
|
+
credentials.isFedrampAccount !== undefined
|
|
834
|
+
&& (
|
|
835
|
+
typeof credentials.isFedrampAccount !== 'boolean'
|
|
836
|
+
|| credentials.isFedrampAccount !== currentTokenInfo.chatgptAccountIsFedramp
|
|
837
|
+
)
|
|
838
|
+
) {
|
|
839
|
+
throw new OpenAiChatGptAuthError('OpenAI ChatGPT FedRAMP status does not match the token claim.');
|
|
840
|
+
}
|
|
841
|
+
const accountId = tokenAccountId;
|
|
842
|
+
const isFedrampAccount = currentTokenInfo.chatgptAccountIsFedramp;
|
|
773
843
|
const headers: Record<string, string> = {
|
|
774
844
|
originator: credentials.originator ?? OPENAI_CHATGPT_DEFAULT_ORIGINATOR,
|
|
775
845
|
};
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import type { IOpenAiModelConnection } from '@modelprofile.com/flexharness-providers/openai';
|
|
2
|
+
import type {
|
|
3
|
+
IOpenAiChatGptAuthCredentials,
|
|
4
|
+
IOpenAiChatGptDynamicModelConnectionOptions,
|
|
5
|
+
} from './interfaces.js';
|
|
6
|
+
import {
|
|
7
|
+
createOpenAiChatGptProviderSettings,
|
|
8
|
+
OPENAI_CHATGPT_CODEX_BASE_URL,
|
|
9
|
+
OpenAiChatGptAuthError,
|
|
10
|
+
} from './smartai.auth.openai.js';
|
|
11
|
+
import { createOpenAiChatGptInstructionsMiddleware } from './smartai.middleware.openai.js';
|
|
12
|
+
|
|
13
|
+
const codexBaseUrl = new URL(OPENAI_CHATGPT_CODEX_BASE_URL);
|
|
14
|
+
|
|
15
|
+
function assertCodexRequestUrl(normalizedUrlArg: string): void {
|
|
16
|
+
const url = new URL(normalizedUrlArg);
|
|
17
|
+
const isCodexPath = url.pathname === codexBaseUrl.pathname
|
|
18
|
+
|| url.pathname.startsWith(`${codexBaseUrl.pathname}/`);
|
|
19
|
+
if (
|
|
20
|
+
url.origin !== codexBaseUrl.origin
|
|
21
|
+
|| url.username !== ''
|
|
22
|
+
|| url.password !== ''
|
|
23
|
+
|| !isCodexPath
|
|
24
|
+
) {
|
|
25
|
+
throw new OpenAiChatGptAuthError('OpenAI ChatGPT inference URL is outside the Codex endpoint.');
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function requireExpectedIdentity(optionsArg: IOpenAiChatGptDynamicModelConnectionOptions): {
|
|
30
|
+
accountId: string | undefined;
|
|
31
|
+
isFedrampAccount: boolean;
|
|
32
|
+
} {
|
|
33
|
+
const expectedIdentity = optionsArg.expectedIdentity;
|
|
34
|
+
if (
|
|
35
|
+
!expectedIdentity
|
|
36
|
+
|| (
|
|
37
|
+
expectedIdentity.accountId !== undefined
|
|
38
|
+
&& (typeof expectedIdentity.accountId !== 'string' || expectedIdentity.accountId.length === 0)
|
|
39
|
+
)
|
|
40
|
+
|| typeof expectedIdentity.isFedrampAccount !== 'boolean'
|
|
41
|
+
) {
|
|
42
|
+
throw new OpenAiChatGptAuthError('OpenAI ChatGPT expected access identity is invalid.');
|
|
43
|
+
}
|
|
44
|
+
return Object.freeze({
|
|
45
|
+
accountId: expectedIdentity.accountId,
|
|
46
|
+
isFedrampAccount: expectedIdentity.isFedrampAccount,
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function assertExpectedIdentity(
|
|
51
|
+
settingsArg: ReturnType<typeof createOpenAiChatGptProviderSettings>,
|
|
52
|
+
expectedIdentityArg: Readonly<{
|
|
53
|
+
accountId: string | undefined;
|
|
54
|
+
isFedrampAccount: boolean;
|
|
55
|
+
}>,
|
|
56
|
+
): void {
|
|
57
|
+
const accountId = settingsArg.headers['ChatGPT-Account-ID'];
|
|
58
|
+
const isFedrampAccount = settingsArg.headers['X-OpenAI-Fedramp'] === 'true';
|
|
59
|
+
if (
|
|
60
|
+
accountId !== expectedIdentityArg.accountId
|
|
61
|
+
|| isFedrampAccount !== expectedIdentityArg.isFedrampAccount
|
|
62
|
+
) {
|
|
63
|
+
throw new OpenAiChatGptAuthError(
|
|
64
|
+
'OpenAI ChatGPT resolved access identity does not match the connection.',
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function applyAccessHeaders(
|
|
70
|
+
headersArg: Headers,
|
|
71
|
+
settingsArg: ReturnType<typeof createOpenAiChatGptProviderSettings>,
|
|
72
|
+
): void {
|
|
73
|
+
headersArg.set('Authorization', `Bearer ${settingsArg.apiKey}`);
|
|
74
|
+
headersArg.set('originator', settingsArg.headers.originator);
|
|
75
|
+
|
|
76
|
+
for (const name of ['ChatGPT-Account-ID', 'X-OpenAI-Fedramp']) {
|
|
77
|
+
const value = settingsArg.headers[name];
|
|
78
|
+
if (value === undefined) {
|
|
79
|
+
headersArg.delete(name);
|
|
80
|
+
} else {
|
|
81
|
+
headersArg.set(name, value);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export const createOpenAiChatGptModelConnection = (
|
|
87
|
+
credentialsArg: IOpenAiChatGptAuthCredentials,
|
|
88
|
+
): IOpenAiModelConnection => ({
|
|
89
|
+
settings: createOpenAiChatGptProviderSettings(credentialsArg),
|
|
90
|
+
middleware: createOpenAiChatGptInstructionsMiddleware(),
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
export const createOpenAiChatGptDynamicModelConnection = (
|
|
94
|
+
optionsArg: IOpenAiChatGptDynamicModelConnectionOptions,
|
|
95
|
+
): IOpenAiModelConnection => {
|
|
96
|
+
if (!optionsArg || typeof optionsArg.resolveAccess !== 'function') {
|
|
97
|
+
throw new OpenAiChatGptAuthError('OpenAI ChatGPT access resolver is required.');
|
|
98
|
+
}
|
|
99
|
+
const expectedIdentity = requireExpectedIdentity(optionsArg);
|
|
100
|
+
const resolveAccess = optionsArg.resolveAccess;
|
|
101
|
+
const originator = optionsArg.originator;
|
|
102
|
+
const transport = optionsArg.fetch ?? globalThis.fetch;
|
|
103
|
+
if (typeof transport !== 'function') {
|
|
104
|
+
throw new OpenAiChatGptAuthError('fetch is not available for OpenAI ChatGPT inference.');
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const fetchWithCurrentAccess: typeof fetch = async (inputArg, initArg) => {
|
|
108
|
+
const request = new Request(inputArg, {
|
|
109
|
+
...initArg,
|
|
110
|
+
redirect: 'error',
|
|
111
|
+
});
|
|
112
|
+
assertCodexRequestUrl(request.url);
|
|
113
|
+
request.signal.throwIfAborted();
|
|
114
|
+
|
|
115
|
+
const access = await resolveAccess({ signal: request.signal });
|
|
116
|
+
request.signal.throwIfAborted();
|
|
117
|
+
const settings = createOpenAiChatGptProviderSettings({
|
|
118
|
+
accessToken: access.accessToken,
|
|
119
|
+
originator,
|
|
120
|
+
});
|
|
121
|
+
if (settings.baseURL !== OPENAI_CHATGPT_CODEX_BASE_URL) {
|
|
122
|
+
throw new OpenAiChatGptAuthError('OpenAI ChatGPT inference endpoint is invalid.');
|
|
123
|
+
}
|
|
124
|
+
assertExpectedIdentity(settings, expectedIdentity);
|
|
125
|
+
|
|
126
|
+
const headers = new Headers(request.headers);
|
|
127
|
+
applyAccessHeaders(headers, settings);
|
|
128
|
+
const authenticatedRequest = new Request(request, {
|
|
129
|
+
headers,
|
|
130
|
+
});
|
|
131
|
+
authenticatedRequest.signal.throwIfAborted();
|
|
132
|
+
return transport(authenticatedRequest);
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
return {
|
|
136
|
+
settings: {
|
|
137
|
+
apiKey: '',
|
|
138
|
+
baseURL: OPENAI_CHATGPT_CODEX_BASE_URL,
|
|
139
|
+
fetch: fetchWithCurrentAccess,
|
|
140
|
+
},
|
|
141
|
+
middleware: createOpenAiChatGptInstructionsMiddleware(),
|
|
142
|
+
};
|
|
143
|
+
};
|
|
@@ -11,6 +11,11 @@ pnpm add @modelprofile.com/flexharness-providers
|
|
|
11
11
|
See the [toolbox usage and migration guide](https://code.foss.global/modelprofile.com/flexharness#readme)
|
|
12
12
|
for composition examples and the SmartAI/SmartAgent migration map.
|
|
13
13
|
|
|
14
|
+
ChatGPT connections come from the `/auth` subpath. Use the dynamic connection
|
|
15
|
+
factory for delegated authority access so every provider HTTP request resolves
|
|
16
|
+
a current token while the connection retains its pinned account and residency
|
|
17
|
+
identity.
|
|
18
|
+
|
|
14
19
|
## Issue Reporting and Security
|
|
15
20
|
|
|
16
21
|
Report issues and security concerns through [community.foss.global](https://community.foss.global).
|
package/ts_providers/readme.md
CHANGED
|
@@ -36,7 +36,11 @@ Provider SDKs are installed together. Authentication, file access and vendor med
|
|
|
36
36
|
are separate entrypoints and are not imported by the provider factory root. For
|
|
37
37
|
ChatGPT authentication, pass
|
|
38
38
|
`connection: createOpenAiChatGptModelConnection(credentials)` from `/auth` to the
|
|
39
|
-
OpenAI model options.
|
|
39
|
+
OpenAI model options. Delegated runtimes use
|
|
40
|
+
`createOpenAiChatGptDynamicModelConnection({ expectedIdentity, resolveAccess })`
|
|
41
|
+
instead; it resolves current access at every provider HTTP request while keeping
|
|
42
|
+
account and residency identity fixed for the connection. API-key inference
|
|
43
|
+
needs no authentication setup.
|
|
40
44
|
|
|
41
45
|
The package includes the applicable OpenAI Codex license and third-party notices.
|
|
42
46
|
|