fix: support OAuth for Gemini media understanding

Extract parseGeminiAuth() to shared infra module and use it in both
embeddings-gemini.ts and inline-data.ts.

Previously, inline-data.ts directly set x-goog-api-key header without
handling OAuth JSON format. Now it properly supports both traditional
API keys and OAuth tokens.
This commit is contained in:
康熙
2026-02-16 14:19:01 +08:00
committed by Peter Steinberger
parent 3379b9d341
commit 153794080e
3 changed files with 47 additions and 38 deletions

40
src/infra/gemini-auth.ts Normal file
View File

@@ -0,0 +1,40 @@
/**
* Shared Gemini authentication utilities.
*
* Supports both traditional API keys and OAuth JSON format.
*/
/**
* Parse Gemini API key and return appropriate auth headers.
*
* OAuth format: `{"token": "...", "projectId": "..."}`
*
* @param apiKey - Either a traditional API key string or OAuth JSON
* @returns Headers object with appropriate authentication
*/
export function parseGeminiAuth(apiKey: string): { headers: Record<string, string> } {
// Try parsing as OAuth JSON format
if (apiKey.startsWith("{")) {
try {
const parsed = JSON.parse(apiKey) as { token?: string; projectId?: string };
if (typeof parsed.token === "string" && parsed.token) {
return {
headers: {
Authorization: `Bearer ${parsed.token}`,
"Content-Type": "application/json",
},
};
}
} catch {
// Parse failed, fallback to API key mode
}
}
// Default: traditional API key
return {
headers: {
"x-goog-api-key": apiKey,
"Content-Type": "application/json",
},
};
}

View File

@@ -1,4 +1,5 @@
import { normalizeGoogleModelId } from "../../../agents/models-config.providers.js";
import { parseGeminiAuth } from "../../../infra/gemini-auth.js";
import { assertOkOrThrowHttpError, fetchWithTimeoutGuarded, normalizeBaseUrl } from "../shared.js";
export async function generateGeminiInlineDataText(params: {
@@ -30,12 +31,12 @@ export async function generateGeminiInlineDataText(params: {
})();
const url = `${baseUrl}/models/${model}:generateContent`;
const authHeaders = parseGeminiAuth(params.apiKey);
const headers = new Headers(params.headers);
if (!headers.has("content-type")) {
headers.set("content-type", "application/json");
}
if (!headers.has("x-goog-api-key")) {
headers.set("x-goog-api-key", params.apiKey);
for (const [key, value] of Object.entries(authHeaders.headers)) {
if (!headers.has(key)) {
headers.set(key, value);
}
}
const prompt = (() => {

View File

@@ -1,6 +1,7 @@
import type { EmbeddingProvider, EmbeddingProviderOptions } from "./embeddings.js";
import { requireApiKey, resolveApiKeyForProvider } from "../agents/model-auth.js";
import { isTruthyEnvValue } from "../infra/env.js";
import { parseGeminiAuth } from "../infra/gemini-auth.js";
import { createSubsystemLogger } from "../logging/subsystem.js";
export type GeminiEmbeddingClient = {
@@ -37,39 +38,6 @@ function resolveRemoteApiKey(remoteApiKey?: string): string | undefined {
return trimmed;
}
/**
* Parse Gemini API key and return appropriate auth headers.
* Supports both traditional API keys and OAuth JSON format.
*
* OAuth format: `{"token": "...", "projectId": "..."}`
*/
function parseGeminiAuth(apiKey: string): { headers: Record<string, string> } {
// Try parsing as OAuth JSON format
if (apiKey.startsWith("{")) {
try {
const parsed = JSON.parse(apiKey) as { token?: string; projectId?: string };
if (typeof parsed.token === "string" && parsed.token) {
return {
headers: {
Authorization: `Bearer ${parsed.token}`,
"Content-Type": "application/json",
},
};
}
} catch {
// Parse failed, fallback to API key mode
}
}
// Default: traditional API key
return {
headers: {
"x-goog-api-key": apiKey,
"Content-Type": "application/json",
},
};
}
function normalizeGeminiModel(model: string): string {
const trimmed = model.trim();
if (!trimmed) {