fix(ai): resolve request-scoped auth before provider calls

closes #6021
This commit is contained in:
Mario Zechner
2026-06-23 23:27:00 +02:00
parent 04fce8099f
commit ef231c4910
5 changed files with 66 additions and 5 deletions
+27 -3
View File
@@ -1,4 +1,4 @@
import type { Api, ImagesApi, ImagesModel, Model } from "../types.ts";
import type { Api, ImagesApi, ImagesModel, Model, ProviderEnv } from "../types.ts";
import type {
ApiKeyAuth,
ApiKeyCredential,
@@ -13,6 +13,11 @@ import type {
export type ModelsErrorCode = "model_source" | "model_validation" | "provider" | "stream" | "auth" | "oauth";
export interface AuthResolutionOverrides {
apiKey?: string;
env?: ProviderEnv;
}
export class ModelsError extends Error {
readonly code: ModelsErrorCode;
@@ -37,20 +42,39 @@ export async function resolveProviderAuth(
model: AuthModel,
credentials: CredentialStore,
authContext: AuthContext,
overrides?: AuthResolutionOverrides,
): Promise<AuthResult | undefined> {
const requestAuthContext = overrides?.env ? overlayEnvAuthContext(authContext, overrides.env) : authContext;
if (overrides?.apiKey !== undefined && provider.auth.apiKey) {
return resolveApiKey(requestAuthContext, provider.auth.apiKey, model, {
type: "api_key",
key: overrides.apiKey,
env: overrides.env,
});
}
const stored = await readCredential(credentials, provider.id);
if (stored) {
if (stored.type === "oauth" && provider.auth.oauth) {
return resolveStoredOAuth(credentials, provider.id, provider.auth.oauth, stored);
}
if (stored.type === "api_key" && provider.auth.apiKey) {
return resolveApiKey(authContext, provider.auth.apiKey, model, stored);
const credential = overrides?.env ? { ...stored, env: { ...stored.env, ...overrides.env } } : stored;
return resolveApiKey(requestAuthContext, provider.auth.apiKey, model, credential);
}
return undefined;
}
// Ambient (env vars, AWS profiles, ADC files).
return provider.auth.apiKey ? resolveApiKey(authContext, provider.auth.apiKey, model, undefined) : undefined;
return provider.auth.apiKey ? resolveApiKey(requestAuthContext, provider.auth.apiKey, model, undefined) : undefined;
}
function overlayEnvAuthContext(base: AuthContext, env: ProviderEnv): AuthContext {
return {
env: async (name) => env[name] || (await base.env(name)),
fileExists: (path) => base.fileExists(path),
};
}
/**
+4 -1
View File
@@ -184,7 +184,10 @@ class ImagesModelsImpl implements MutableImagesModels {
throw new ModelsError("provider", `Unknown provider: ${model.provider}`);
}
const resolution = await this.getAuth(model);
const resolution = await resolveProviderAuth(provider, model, this.credentials, this.authContext, {
apiKey: options?.apiKey,
env: options?.env,
});
const auth = resolution?.auth;
if (!auth) {
return provider.generateImages(model, context, options);
+10 -1
View File
@@ -231,7 +231,16 @@ class ModelsImpl implements MutableModels {
model: Model<Api>,
options: TOptions | undefined,
): Promise<{ requestModel: Model<Api>; requestOptions: TOptions | undefined }> {
const resolution = await this.getAuth(model);
const resolution = await resolveProviderAuth(
this.requireProvider(model),
model,
this.credentials,
this.authContext,
{
apiKey: options?.apiKey,
env: options?.env,
},
);
const auth = resolution?.auth;
if (!auth) return { requestModel: model, requestOptions: options };