Skip to content

Conversation

deyaaeldeen
Copy link
Contributor

  • I understand that this repository is auto-generated and my pull request may not be merged

Changes being requested

This PR introduces a new tokenProvider option to the OpenAI client, enabling dynamic token retrieval for authentication scenarios where API keys need to be refreshed or obtained at runtime.

Additional context & links

Changes

New Feature: tokenProvider Option

  • Added TokenProvider type definition: () => Promise<AccessToken>
  • Added AccessToken interface with token: string property
  • Added tokenProvider option to ClientOptions interface
  • Implemented token provider logic in the _setToken() method

Key Benefits

  1. Dynamic Authentication: Supports scenarios where tokens expire and need to be refreshed
  2. Enhanced Security: Tokens can be retrieved just-in-time rather than stored statically
  3. Enterprise Integration: Enables integration with corporate identity providers and token management systems
  4. Flexible Authentication: Provides an alternative to static API keys for environments requiring dynamic credentials

Implementation Details

Client Construction:

  • tokenProvider and apiKey are mutually exclusive - only one can be provided
  • If neither is provided, an error is thrown requiring one of the two authentication methods
  • Browser usage automatically allowed when using tokenProvider (sets dangerouslyAllowBrowser: true)

Token Management:

  • _setToken() method now returns a boolean indicating if a token was successfully retrieved
  • Token provider is called on every request preparation via prepareOptions()

Usage Example

const client = new OpenAI({
  tokenProvider: async () => {
    // Fetch token from your auth service
    const response = await fetch('/api/auth/token');
    const data = await response.json();
    return { token: data.access_token };
  }
});

Backward Compatibility

This change is fully backward compatible. Existing code using apiKey continues to work unchanged. The new tokenProvider option is purely additive.


@deyaaeldeen deyaaeldeen force-pushed the feat/token-provider branch 2 times, most recently from b4dd476 to f514473 Compare July 16, 2025 23:40
src/client.ts Outdated
@@ -438,6 +457,31 @@ export class OpenAI {
return Errors.APIError.generate(status, error, message, headers);
}

async _setToken(): Promise<boolean> {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: I think "refresh" is a better verb than "set" here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The decision to refresh is taken by the input function and not by this method, i.e. it is not guaranteed that the token will be refreshed if this method is called. However, let me know if you feel strongly about it and I can update the name.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do dislike _set but don't feel strongly about it being called _refresh - I find it weird that a method called _setX() doesn't actually take any arguments.

fwiw I was also framing "refresh" as in, refresh the apiKey property - not necessarily do a full token refresh.

wdyt about _callApiKey()? I don't think that's great either... my main fud with _setApiKey() is that the return value is not intuitive imo and it'd be great if we could find a name that makes that work

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 577ebc6.

@deyaaeldeen deyaaeldeen force-pushed the feat/token-provider branch 2 times, most recently from 1453757 to b623760 Compare August 22, 2025 18:09
export interface ClientOptions {
/**
* Defaults to process.env['OPENAI_API_KEY'].
*/
apiKey?: string | undefined;

apiKey?: string | ApiKeySetter | undefined;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The docstring should mention the new function behaviour

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point, addressed in 577ebc6.

@@ -385,7 +386,7 @@ export class OpenAI {

this._options = options;

this.apiKey = apiKey;
this.apiKey = typeof apiKey === 'string' ? apiKey : 'Missing Key';
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: should probably be something like <not set yet>?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

actually, we should move API_KEY_SENTINEL our of the azure specific file and use that

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is only used once here so I am not sure if there is value to have it in a constant var.

let fail = true;
const testFetch = async (url: RequestInfo, req: RequestInit | undefined): Promise<Response> => {
const testFetch = async (url: any, { headers }: RequestInit = {}): Promise<Response> => {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

q: why did you have to use any?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point, addressed in 577ebc6.

src/client.ts Outdated
@@ -438,6 +457,31 @@ export class OpenAI {
return Errors.APIError.generate(status, error, message, headers);
}

async _setToken(): Promise<boolean> {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do dislike _set but don't feel strongly about it being called _refresh - I find it weird that a method called _setX() doesn't actually take any arguments.

fwiw I was also framing "refresh" as in, refresh the apiKey property - not necessarily do a full token refresh.

wdyt about _callApiKey()? I don't think that's great either... my main fud with _setApiKey() is that the return value is not intuitive imo and it'd be great if we could find a name that makes that work

@@ -49,6 +50,10 @@ export class OpenAIRealtimeWebSocket extends OpenAIRealtimeEmitter {

client ??= new OpenAI({ dangerouslyAllowBrowser });

if (typeof (client as any)?._options?.apiKey !== 'string') {
throw new Error('Call the create method instead to construct the client');
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: this error message could be more helpful

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also, wait does this work? because _setApiKey() doesn't mutate options, won't this check always return true if you pass in a function?

should we instead define the client.apiKey fallback string as a constant and check that? e.g. if (client.apiKey === API_KEY_MISSING) { ...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We want to check whether the user provided a function or a string at the time of constructing the OpenAI client. If the user provided a function, we can't call it here. if (client.apiKey === API_KEY_MISSING) wouldn't work because _setApiKey might have been called setting client.apiKey to a token that might have expired by the time the realtime client is being used.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

error messages are updated in 390c131

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good point about the token potentially being expired but I think my other question still stands? does this check work as intended? because we don't mutate _options, so if you originally pass in a function then typeof (client as any)?._options?.apiKey !== 'string' would always be true?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, that is the intention. If the user passed in a function, they can't construct an RT client using the constructor.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh I see what you mean now. The constructor is called in the factory method too. Let me address this.

src/client.ts Outdated
Comment on lines 460 to 478
try {
const token = await apiKey();
if (!token || typeof token !== 'string') {
throw new Errors.OpenAIError(
`Expected 'apiKey' function argument to return a string but it returned ${token}`,
);
}
this.apiKey = token;
return true;
} catch (err: any) {
if (err instanceof Errors.OpenAIError) {
throw err;
}
throw new Errors.OpenAIError(
`Failed to get token from 'apiKey' function: ${err.message}`,
// @ts-ignore
{ cause: err },
);
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

having a try catch around the whole block and then checking inside the catch if we threw isn't great, could we instead just either put the try catch just around the await apiKey()? or await apiKey().catch(...)?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point, addressed in 577ebc6.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants