opencode-gemini-auth 1.3.10 → 1.4.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/README.md +38 -0
- package/package.json +5 -2
- package/src/plugin/auth.test.ts +58 -0
- package/src/plugin/oauth-authorize.ts +198 -0
- package/src/plugin/project/api.ts +202 -0
- package/src/plugin/project/context.ts +187 -0
- package/src/plugin/project/index.ts +6 -0
- package/src/plugin/project/types.ts +67 -0
- package/src/plugin/project/utils.ts +120 -0
- package/src/plugin/quota.test.ts +62 -0
- package/src/plugin/quota.ts +182 -0
- package/src/plugin/request/identifiers.ts +100 -0
- package/src/plugin/request/index.ts +3 -0
- package/src/plugin/request/openai.ts +128 -0
- package/src/plugin/request/prepare.ts +190 -0
- package/src/plugin/request/response.ts +191 -0
- package/src/plugin/request/shared.ts +72 -0
- package/src/plugin/{request-helpers.ts → request-helpers/errors.ts} +34 -213
- package/src/plugin/request-helpers/index.ts +12 -0
- package/src/plugin/request-helpers/parsing.ts +44 -0
- package/src/plugin/request-helpers/thinking.ts +36 -0
- package/src/plugin/request-helpers/types.ts +78 -0
- package/src/plugin/request-helpers.test.ts +84 -0
- package/src/plugin/request.test.ts +91 -0
- package/src/plugin/retry/helpers.ts +175 -0
- package/src/plugin/retry/index.ts +81 -0
- package/src/plugin/retry/quota.ts +210 -0
- package/src/plugin/retry.test.ts +106 -0
- package/src/plugin/token.test.ts +31 -0
- package/src/plugin/token.ts +24 -0
- package/src/plugin/types.ts +4 -0
- package/src/plugin.ts +130 -587
- package/src/plugin/project.ts +0 -551
- package/src/plugin/request.ts +0 -483
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
|
|
3
|
+
import { isRecord, pickString } from "./shared";
|
|
4
|
+
|
|
5
|
+
const PROCESS_SESSION_ID = randomUUID();
|
|
6
|
+
|
|
7
|
+
function resolveUserPromptId(payload: Record<string, unknown>, request?: Record<string, unknown>): string {
|
|
8
|
+
const extra = isRecord(payload.extra_body) ? payload.extra_body : undefined;
|
|
9
|
+
|
|
10
|
+
return (
|
|
11
|
+
pickString(
|
|
12
|
+
payload.user_prompt_id,
|
|
13
|
+
payload.userPromptId,
|
|
14
|
+
payload.prompt_id,
|
|
15
|
+
payload.promptId,
|
|
16
|
+
payload.request_id,
|
|
17
|
+
payload.requestId,
|
|
18
|
+
request?.user_prompt_id,
|
|
19
|
+
request?.userPromptId,
|
|
20
|
+
request?.prompt_id,
|
|
21
|
+
request?.promptId,
|
|
22
|
+
request?.request_id,
|
|
23
|
+
request?.requestId,
|
|
24
|
+
extra?.user_prompt_id,
|
|
25
|
+
extra?.userPromptId,
|
|
26
|
+
extra?.prompt_id,
|
|
27
|
+
extra?.promptId,
|
|
28
|
+
extra?.request_id,
|
|
29
|
+
extra?.requestId,
|
|
30
|
+
) ?? randomUUID()
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function resolveSessionId(payload: Record<string, unknown>, request?: Record<string, unknown>): string {
|
|
35
|
+
const extra = isRecord(payload.extra_body) ? payload.extra_body : undefined;
|
|
36
|
+
return (
|
|
37
|
+
pickString(
|
|
38
|
+
request?.session_id,
|
|
39
|
+
request?.sessionId,
|
|
40
|
+
payload.session_id,
|
|
41
|
+
payload.sessionId,
|
|
42
|
+
extra?.session_id,
|
|
43
|
+
extra?.sessionId,
|
|
44
|
+
) ?? PROCESS_SESSION_ID
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function stripPromptIdentifierAliases(payload: Record<string, unknown>): void {
|
|
49
|
+
delete payload.user_prompt_id;
|
|
50
|
+
delete payload.userPromptId;
|
|
51
|
+
delete payload.prompt_id;
|
|
52
|
+
delete payload.promptId;
|
|
53
|
+
delete payload.request_id;
|
|
54
|
+
delete payload.requestId;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function stripSessionIdentifierAliases(payload: Record<string, unknown>): void {
|
|
58
|
+
delete payload.sessionId;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Applies canonical identifiers for wrapped Code Assist payloads.
|
|
63
|
+
*
|
|
64
|
+
* `user_prompt_id` and `session_id` are first-class identifiers in Gemini CLI
|
|
65
|
+
* request envelopes used for traceability, usage accounting, and support diagnostics.
|
|
66
|
+
*/
|
|
67
|
+
export function normalizeWrappedIdentifiers(
|
|
68
|
+
wrapped: Record<string, unknown>,
|
|
69
|
+
): { userPromptId: string; sessionId: string } {
|
|
70
|
+
const request = isRecord(wrapped.request) ? { ...wrapped.request } : {};
|
|
71
|
+
const userPromptId = resolveUserPromptId(wrapped, request);
|
|
72
|
+
const sessionId = resolveSessionId(wrapped, request);
|
|
73
|
+
|
|
74
|
+
request.session_id = sessionId;
|
|
75
|
+
stripSessionIdentifierAliases(request);
|
|
76
|
+
wrapped.request = request;
|
|
77
|
+
|
|
78
|
+
wrapped.user_prompt_id = userPromptId;
|
|
79
|
+
stripPromptIdentifierAliases(wrapped);
|
|
80
|
+
|
|
81
|
+
return { userPromptId, sessionId };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Applies canonical identifiers for unwrapped request payloads before wrapping.
|
|
86
|
+
*
|
|
87
|
+
* We normalize aliases here so downstream logic has a single source of truth.
|
|
88
|
+
*/
|
|
89
|
+
export function normalizeRequestPayloadIdentifiers(
|
|
90
|
+
payload: Record<string, unknown>,
|
|
91
|
+
): { userPromptId: string; sessionId: string } {
|
|
92
|
+
const userPromptId = resolveUserPromptId(payload);
|
|
93
|
+
const sessionId = resolveSessionId(payload);
|
|
94
|
+
|
|
95
|
+
payload.session_id = sessionId;
|
|
96
|
+
stripSessionIdentifierAliases(payload);
|
|
97
|
+
stripPromptIdentifierAliases(payload);
|
|
98
|
+
|
|
99
|
+
return { userPromptId, sessionId };
|
|
100
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
interface GeminiFunctionCallPart {
|
|
2
|
+
functionCall?: {
|
|
3
|
+
name: string;
|
|
4
|
+
args?: Record<string, unknown>;
|
|
5
|
+
[key: string]: unknown;
|
|
6
|
+
};
|
|
7
|
+
thoughtSignature?: string;
|
|
8
|
+
[key: string]: unknown;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
interface OpenAIToolCall {
|
|
12
|
+
function?: {
|
|
13
|
+
name?: string;
|
|
14
|
+
arguments?: string;
|
|
15
|
+
[key: string]: unknown;
|
|
16
|
+
};
|
|
17
|
+
[key: string]: unknown;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface OpenAIMessage {
|
|
21
|
+
content?: string | null;
|
|
22
|
+
tool_calls?: OpenAIToolCall[];
|
|
23
|
+
[key: string]: unknown;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Transforms OpenAI `tool_calls` to Gemini `functionCall` parts.
|
|
28
|
+
*/
|
|
29
|
+
export function transformOpenAIToolCalls(requestPayload: Record<string, unknown>): void {
|
|
30
|
+
const messages = requestPayload.messages;
|
|
31
|
+
if (!messages || !Array.isArray(messages)) {
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
for (const message of messages) {
|
|
36
|
+
if (!message || typeof message !== "object") {
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const msgObj = message as OpenAIMessage;
|
|
41
|
+
const toolCalls = msgObj.tool_calls;
|
|
42
|
+
if (!toolCalls || !Array.isArray(toolCalls) || toolCalls.length === 0) {
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const parts: GeminiFunctionCallPart[] = [];
|
|
47
|
+
if (typeof msgObj.content === "string" && msgObj.content.length > 0) {
|
|
48
|
+
parts.push({ text: msgObj.content });
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
for (const toolCall of toolCalls) {
|
|
52
|
+
if (!toolCall || typeof toolCall !== "object") {
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const fn = toolCall.function;
|
|
57
|
+
if (!fn || typeof fn !== "object") {
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const name = fn.name;
|
|
62
|
+
const args = parseJsonObject(fn.arguments);
|
|
63
|
+
parts.push({
|
|
64
|
+
functionCall: {
|
|
65
|
+
name: name ?? "",
|
|
66
|
+
args,
|
|
67
|
+
},
|
|
68
|
+
thoughtSignature: "skip_thought_signature_validator",
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
msgObj.parts = parts;
|
|
73
|
+
delete msgObj.tool_calls;
|
|
74
|
+
delete msgObj.content;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Adds synthetic thoughtSignature to function calls in both flat and wrapped payloads.
|
|
80
|
+
*/
|
|
81
|
+
export function addThoughtSignaturesToFunctionCalls(requestPayload: Record<string, unknown>): void {
|
|
82
|
+
const processContents = (contents: unknown): void => {
|
|
83
|
+
if (!contents || !Array.isArray(contents)) {
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
for (const content of contents) {
|
|
88
|
+
if (!content || typeof content !== "object") {
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const parts = (content as Record<string, unknown>).parts;
|
|
93
|
+
if (!parts || !Array.isArray(parts)) {
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
for (const part of parts) {
|
|
98
|
+
if (!part || typeof part !== "object") {
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
const partObj = part as Record<string, unknown>;
|
|
102
|
+
if (partObj.functionCall && !partObj.thoughtSignature) {
|
|
103
|
+
partObj.thoughtSignature = "skip_thought_signature_validator";
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
processContents(requestPayload.contents);
|
|
110
|
+
if (requestPayload.request && typeof requestPayload.request === "object") {
|
|
111
|
+
processContents((requestPayload.request as Record<string, unknown>).contents);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function parseJsonObject(value: unknown): Record<string, unknown> {
|
|
116
|
+
if (typeof value !== "string") {
|
|
117
|
+
return {};
|
|
118
|
+
}
|
|
119
|
+
try {
|
|
120
|
+
const parsed = JSON.parse(value);
|
|
121
|
+
if (parsed && typeof parsed === "object") {
|
|
122
|
+
return parsed as Record<string, unknown>;
|
|
123
|
+
}
|
|
124
|
+
return {};
|
|
125
|
+
} catch {
|
|
126
|
+
return {};
|
|
127
|
+
}
|
|
128
|
+
}
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
|
|
3
|
+
import { CODE_ASSIST_HEADERS, GEMINI_CODE_ASSIST_ENDPOINT } from "../../constants";
|
|
4
|
+
import { normalizeThinkingConfig } from "../request-helpers";
|
|
5
|
+
import { normalizeRequestPayloadIdentifiers, normalizeWrappedIdentifiers } from "./identifiers";
|
|
6
|
+
import { addThoughtSignaturesToFunctionCalls, transformOpenAIToolCalls } from "./openai";
|
|
7
|
+
import { isGenerativeLanguageRequest, toRequestUrlString } from "./shared";
|
|
8
|
+
|
|
9
|
+
const STREAM_ACTION = "streamGenerateContent";
|
|
10
|
+
const MODEL_FALLBACKS: Record<string, string> = {
|
|
11
|
+
"gemini-2.5-flash-image": "gemini-2.5-flash",
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Rewrites OpenAI-style requests into Gemini Code Assist request shape.
|
|
16
|
+
*/
|
|
17
|
+
export function prepareGeminiRequest(
|
|
18
|
+
input: RequestInfo,
|
|
19
|
+
init: RequestInit | undefined,
|
|
20
|
+
accessToken: string,
|
|
21
|
+
projectId: string,
|
|
22
|
+
): {
|
|
23
|
+
request: RequestInfo;
|
|
24
|
+
init: RequestInit;
|
|
25
|
+
streaming: boolean;
|
|
26
|
+
requestedModel?: string;
|
|
27
|
+
} {
|
|
28
|
+
const baseInit: RequestInit = { ...init };
|
|
29
|
+
const headers = new Headers(init?.headers ?? {});
|
|
30
|
+
|
|
31
|
+
if (!isGenerativeLanguageRequest(input)) {
|
|
32
|
+
return {
|
|
33
|
+
request: input,
|
|
34
|
+
init: { ...baseInit, headers },
|
|
35
|
+
streaming: false,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
headers.set("Authorization", `Bearer ${accessToken}`);
|
|
40
|
+
headers.delete("x-api-key");
|
|
41
|
+
|
|
42
|
+
const match = toRequestUrlString(input).match(/\/models\/([^:]+):(\w+)/);
|
|
43
|
+
if (!match) {
|
|
44
|
+
return {
|
|
45
|
+
request: input,
|
|
46
|
+
init: { ...baseInit, headers },
|
|
47
|
+
streaming: false,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const [, rawModel = "", rawAction = ""] = match;
|
|
52
|
+
const effectiveModel = MODEL_FALLBACKS[rawModel] ?? rawModel;
|
|
53
|
+
const streaming = rawAction === STREAM_ACTION;
|
|
54
|
+
const transformedUrl = `${GEMINI_CODE_ASSIST_ENDPOINT}/v1internal:${rawAction}${
|
|
55
|
+
streaming ? "?alt=sse" : ""
|
|
56
|
+
}`;
|
|
57
|
+
|
|
58
|
+
let body = baseInit.body;
|
|
59
|
+
let requestIdentifier: string = randomUUID();
|
|
60
|
+
|
|
61
|
+
if (typeof baseInit.body === "string" && baseInit.body) {
|
|
62
|
+
const transformed = transformRequestBody(baseInit.body, projectId, effectiveModel);
|
|
63
|
+
if (transformed.body) {
|
|
64
|
+
body = transformed.body;
|
|
65
|
+
requestIdentifier = transformed.userPromptId;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (streaming) {
|
|
70
|
+
headers.set("Accept", "text/event-stream");
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
headers.set("User-Agent", CODE_ASSIST_HEADERS["User-Agent"]);
|
|
74
|
+
headers.set("X-Goog-Api-Client", CODE_ASSIST_HEADERS["X-Goog-Api-Client"]);
|
|
75
|
+
headers.set("Client-Metadata", CODE_ASSIST_HEADERS["Client-Metadata"]);
|
|
76
|
+
/**
|
|
77
|
+
* Request-scoped identifier used by Gemini CLI tooling and backend traces.
|
|
78
|
+
* We keep this aligned so quota/debug triage can correlate client and server events.
|
|
79
|
+
*/
|
|
80
|
+
headers.set("x-activity-request-id", requestIdentifier);
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
request: transformedUrl,
|
|
84
|
+
init: {
|
|
85
|
+
...baseInit,
|
|
86
|
+
headers,
|
|
87
|
+
body,
|
|
88
|
+
},
|
|
89
|
+
streaming,
|
|
90
|
+
requestedModel: rawModel,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function transformRequestBody(
|
|
95
|
+
body: string,
|
|
96
|
+
projectId: string,
|
|
97
|
+
effectiveModel: string,
|
|
98
|
+
): { body?: string; userPromptId: string } {
|
|
99
|
+
const fallbackId = randomUUID();
|
|
100
|
+
try {
|
|
101
|
+
const parsedBody = JSON.parse(body) as Record<string, unknown>;
|
|
102
|
+
const isWrapped = typeof parsedBody.project === "string" && "request" in parsedBody;
|
|
103
|
+
|
|
104
|
+
if (isWrapped) {
|
|
105
|
+
const wrappedBody = {
|
|
106
|
+
...parsedBody,
|
|
107
|
+
model: effectiveModel,
|
|
108
|
+
} as Record<string, unknown>;
|
|
109
|
+
const { userPromptId } = normalizeWrappedIdentifiers(wrappedBody);
|
|
110
|
+
return { body: JSON.stringify(wrappedBody), userPromptId };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const requestPayload = { ...parsedBody };
|
|
114
|
+
transformOpenAIToolCalls(requestPayload);
|
|
115
|
+
addThoughtSignaturesToFunctionCalls(requestPayload);
|
|
116
|
+
normalizeThinking(requestPayload);
|
|
117
|
+
normalizeSystemInstruction(requestPayload);
|
|
118
|
+
normalizeCachedContent(requestPayload);
|
|
119
|
+
|
|
120
|
+
if ("model" in requestPayload) {
|
|
121
|
+
delete requestPayload.model;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const { userPromptId } = normalizeRequestPayloadIdentifiers(requestPayload);
|
|
125
|
+
const wrappedBody = {
|
|
126
|
+
project: projectId,
|
|
127
|
+
model: effectiveModel,
|
|
128
|
+
user_prompt_id: userPromptId,
|
|
129
|
+
request: requestPayload,
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
return { body: JSON.stringify(wrappedBody), userPromptId };
|
|
133
|
+
} catch (error) {
|
|
134
|
+
console.error("Failed to transform Gemini request body:", error);
|
|
135
|
+
return { userPromptId: fallbackId };
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function normalizeThinking(requestPayload: Record<string, unknown>): void {
|
|
140
|
+
const rawGenerationConfig = requestPayload.generationConfig as Record<string, unknown> | undefined;
|
|
141
|
+
const normalizedThinking = normalizeThinkingConfig(rawGenerationConfig?.thinkingConfig);
|
|
142
|
+
if (normalizedThinking) {
|
|
143
|
+
if (rawGenerationConfig) {
|
|
144
|
+
rawGenerationConfig.thinkingConfig = normalizedThinking;
|
|
145
|
+
requestPayload.generationConfig = rawGenerationConfig;
|
|
146
|
+
} else {
|
|
147
|
+
requestPayload.generationConfig = { thinkingConfig: normalizedThinking };
|
|
148
|
+
}
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (rawGenerationConfig?.thinkingConfig) {
|
|
153
|
+
delete rawGenerationConfig.thinkingConfig;
|
|
154
|
+
requestPayload.generationConfig = rawGenerationConfig;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function normalizeSystemInstruction(requestPayload: Record<string, unknown>): void {
|
|
159
|
+
if ("system_instruction" in requestPayload) {
|
|
160
|
+
requestPayload.systemInstruction = requestPayload.system_instruction;
|
|
161
|
+
delete requestPayload.system_instruction;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function normalizeCachedContent(requestPayload: Record<string, unknown>): void {
|
|
166
|
+
const extraBody =
|
|
167
|
+
requestPayload.extra_body && typeof requestPayload.extra_body === "object"
|
|
168
|
+
? (requestPayload.extra_body as Record<string, unknown>)
|
|
169
|
+
: undefined;
|
|
170
|
+
const cachedContentFromExtra = extraBody?.cached_content ?? extraBody?.cachedContent;
|
|
171
|
+
const cachedContent =
|
|
172
|
+
(requestPayload.cached_content as string | undefined) ??
|
|
173
|
+
(requestPayload.cachedContent as string | undefined) ??
|
|
174
|
+
(cachedContentFromExtra as string | undefined);
|
|
175
|
+
|
|
176
|
+
if (cachedContent) {
|
|
177
|
+
requestPayload.cachedContent = cachedContent;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
delete requestPayload.cached_content;
|
|
181
|
+
if (!extraBody) {
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
delete extraBody.cached_content;
|
|
186
|
+
delete extraBody.cachedContent;
|
|
187
|
+
if (Object.keys(extraBody).length === 0) {
|
|
188
|
+
delete requestPayload.extra_body;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { logGeminiDebugResponse, type GeminiDebugContext } from "../debug";
|
|
2
|
+
import {
|
|
3
|
+
enhanceGeminiErrorResponse,
|
|
4
|
+
extractUsageMetadata,
|
|
5
|
+
parseGeminiApiBody,
|
|
6
|
+
rewriteGeminiPreviewAccessError,
|
|
7
|
+
type GeminiApiBody,
|
|
8
|
+
} from "../request-helpers";
|
|
9
|
+
import { injectResponseIdFromTrace } from "./shared";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Normalizes Gemini responses, preserving request metadata and usage counters.
|
|
13
|
+
*/
|
|
14
|
+
export async function transformGeminiResponse(
|
|
15
|
+
response: Response,
|
|
16
|
+
streaming: boolean,
|
|
17
|
+
debugContext?: GeminiDebugContext | null,
|
|
18
|
+
requestedModel?: string,
|
|
19
|
+
): Promise<Response> {
|
|
20
|
+
const contentType = response.headers.get("content-type") ?? "";
|
|
21
|
+
const isJsonResponse = contentType.includes("application/json");
|
|
22
|
+
const isEventStreamResponse = contentType.includes("text/event-stream");
|
|
23
|
+
|
|
24
|
+
if (!isJsonResponse && !isEventStreamResponse) {
|
|
25
|
+
logGeminiDebugResponse(debugContext, response, {
|
|
26
|
+
note: "Non-JSON response (body omitted)",
|
|
27
|
+
});
|
|
28
|
+
return response;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
try {
|
|
32
|
+
const headers = new Headers(response.headers);
|
|
33
|
+
|
|
34
|
+
if (streaming && response.ok && isEventStreamResponse && response.body) {
|
|
35
|
+
logGeminiDebugResponse(debugContext, response, {
|
|
36
|
+
note: "Streaming SSE payload (body omitted)",
|
|
37
|
+
headersOverride: headers,
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
return new Response(transformStreamingPayloadStream(response.body), {
|
|
41
|
+
status: response.status,
|
|
42
|
+
statusText: response.statusText,
|
|
43
|
+
headers,
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const text = await response.text();
|
|
48
|
+
const init = {
|
|
49
|
+
status: response.status,
|
|
50
|
+
statusText: response.statusText,
|
|
51
|
+
headers,
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const parsed: GeminiApiBody | null = !streaming || !isEventStreamResponse ? parseGeminiApiBody(text) : null;
|
|
55
|
+
const enhanced = !response.ok && parsed ? enhanceGeminiErrorResponse(parsed, response.status) : null;
|
|
56
|
+
if (enhanced?.retryAfterMs) {
|
|
57
|
+
const retryAfterSec = Math.ceil(enhanced.retryAfterMs / 1000).toString();
|
|
58
|
+
headers.set("Retry-After", retryAfterSec);
|
|
59
|
+
headers.set("retry-after-ms", String(enhanced.retryAfterMs));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const previewPatched = parsed
|
|
63
|
+
? rewriteGeminiPreviewAccessError(enhanced?.body ?? parsed, response.status, requestedModel)
|
|
64
|
+
: null;
|
|
65
|
+
|
|
66
|
+
const effectiveBodyRaw = previewPatched ?? enhanced?.body ?? parsed ?? undefined;
|
|
67
|
+
const effectiveBody =
|
|
68
|
+
effectiveBodyRaw && typeof effectiveBodyRaw === "object"
|
|
69
|
+
? injectResponseIdFromTrace(effectiveBodyRaw as Record<string, unknown>)
|
|
70
|
+
: effectiveBodyRaw;
|
|
71
|
+
|
|
72
|
+
attachUsageHeaders(headers, effectiveBody);
|
|
73
|
+
|
|
74
|
+
logGeminiDebugResponse(debugContext, response, {
|
|
75
|
+
body: text,
|
|
76
|
+
note: streaming ? "Streaming SSE payload (buffered)" : undefined,
|
|
77
|
+
headersOverride: headers,
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
if (!parsed) {
|
|
81
|
+
return new Response(text, init);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (effectiveBody && typeof effectiveBody === "object" && "response" in effectiveBody) {
|
|
85
|
+
return new Response(JSON.stringify((effectiveBody as { response: unknown }).response), init);
|
|
86
|
+
}
|
|
87
|
+
if (previewPatched) {
|
|
88
|
+
return new Response(JSON.stringify(previewPatched), init);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return new Response(text, init);
|
|
92
|
+
} catch (error) {
|
|
93
|
+
logGeminiDebugResponse(debugContext, response, {
|
|
94
|
+
error,
|
|
95
|
+
note: "Failed to transform Gemini response",
|
|
96
|
+
});
|
|
97
|
+
console.error("Failed to transform Gemini response:", error);
|
|
98
|
+
return response;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function attachUsageHeaders(headers: Headers, effectiveBody: unknown): void {
|
|
103
|
+
if (!effectiveBody || typeof effectiveBody !== "object") {
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
const usage = extractUsageMetadata(effectiveBody as GeminiApiBody);
|
|
107
|
+
if (usage?.cachedContentTokenCount === undefined) {
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
headers.set("x-gemini-cached-content-token-count", String(usage.cachedContentTokenCount));
|
|
112
|
+
if (usage.totalTokenCount !== undefined) {
|
|
113
|
+
headers.set("x-gemini-total-token-count", String(usage.totalTokenCount));
|
|
114
|
+
}
|
|
115
|
+
if (usage.promptTokenCount !== undefined) {
|
|
116
|
+
headers.set("x-gemini-prompt-token-count", String(usage.promptTokenCount));
|
|
117
|
+
}
|
|
118
|
+
if (usage.candidatesTokenCount !== undefined) {
|
|
119
|
+
headers.set("x-gemini-candidates-token-count", String(usage.candidatesTokenCount));
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function transformStreamingPayloadStream(stream: ReadableStream<Uint8Array>): ReadableStream<Uint8Array> {
|
|
124
|
+
const decoder = new TextDecoder();
|
|
125
|
+
const encoder = new TextEncoder();
|
|
126
|
+
let buffer = "";
|
|
127
|
+
let reader: ReadableStreamDefaultReader<Uint8Array> | null = null;
|
|
128
|
+
|
|
129
|
+
return new ReadableStream<Uint8Array>({
|
|
130
|
+
start(controller) {
|
|
131
|
+
reader = stream.getReader();
|
|
132
|
+
const pump = (): void => {
|
|
133
|
+
reader!
|
|
134
|
+
.read()
|
|
135
|
+
.then(({ done, value }) => {
|
|
136
|
+
if (done) {
|
|
137
|
+
buffer += decoder.decode();
|
|
138
|
+
if (buffer.length > 0) {
|
|
139
|
+
controller.enqueue(encoder.encode(transformStreamingLine(buffer)));
|
|
140
|
+
}
|
|
141
|
+
controller.close();
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
buffer += decoder.decode(value, { stream: true });
|
|
146
|
+
let newlineIndex = buffer.indexOf("\n");
|
|
147
|
+
while (newlineIndex !== -1) {
|
|
148
|
+
const line = buffer.slice(0, newlineIndex);
|
|
149
|
+
buffer = buffer.slice(newlineIndex + 1);
|
|
150
|
+
const hasCr = line.endsWith("\r");
|
|
151
|
+
const rawLine = hasCr ? line.slice(0, -1) : line;
|
|
152
|
+
const transformed = transformStreamingLine(rawLine);
|
|
153
|
+
controller.enqueue(encoder.encode(`${transformed}${hasCr ? "\r\n" : "\n"}`));
|
|
154
|
+
newlineIndex = buffer.indexOf("\n");
|
|
155
|
+
}
|
|
156
|
+
pump();
|
|
157
|
+
})
|
|
158
|
+
.catch((error) => {
|
|
159
|
+
controller.error(error);
|
|
160
|
+
});
|
|
161
|
+
};
|
|
162
|
+
pump();
|
|
163
|
+
},
|
|
164
|
+
cancel(reason) {
|
|
165
|
+
if (reader) {
|
|
166
|
+
reader.cancel(reason).catch(() => {});
|
|
167
|
+
}
|
|
168
|
+
},
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function transformStreamingLine(line: string): string {
|
|
173
|
+
if (!line.startsWith("data:")) {
|
|
174
|
+
return line;
|
|
175
|
+
}
|
|
176
|
+
const json = line.slice(5).trim();
|
|
177
|
+
if (!json) {
|
|
178
|
+
return line;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
try {
|
|
182
|
+
const parsed = JSON.parse(json) as Record<string, unknown>;
|
|
183
|
+
const patched = injectResponseIdFromTrace(parsed);
|
|
184
|
+
if (patched.response !== undefined) {
|
|
185
|
+
return `data: ${JSON.stringify(patched.response)}`;
|
|
186
|
+
}
|
|
187
|
+
} catch {
|
|
188
|
+
return line;
|
|
189
|
+
}
|
|
190
|
+
return line;
|
|
191
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Returns a URL string for supported RequestInfo inputs.
|
|
3
|
+
*/
|
|
4
|
+
export function toRequestUrlString(value: RequestInfo): string {
|
|
5
|
+
if (typeof value === "string") {
|
|
6
|
+
return value;
|
|
7
|
+
}
|
|
8
|
+
if (value instanceof URL) {
|
|
9
|
+
return value.toString();
|
|
10
|
+
}
|
|
11
|
+
const candidate = (value as Request).url;
|
|
12
|
+
if (candidate) {
|
|
13
|
+
return candidate;
|
|
14
|
+
}
|
|
15
|
+
return value.toString();
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Detects Gemini/Generative Language API requests by URL.
|
|
20
|
+
*/
|
|
21
|
+
export function isGenerativeLanguageRequest(input: RequestInfo): input is string {
|
|
22
|
+
return toRequestUrlString(input).includes("generativelanguage.googleapis.com");
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function isRecord(value: unknown): value is Record<string, unknown> {
|
|
26
|
+
return !!value && typeof value === "object";
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function readString(value: unknown): string | undefined {
|
|
30
|
+
if (typeof value !== "string") {
|
|
31
|
+
return undefined;
|
|
32
|
+
}
|
|
33
|
+
const trimmed = value.trim();
|
|
34
|
+
return trimmed.length > 0 ? trimmed : undefined;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function pickString(...values: unknown[]): string | undefined {
|
|
38
|
+
for (const value of values) {
|
|
39
|
+
const str = readString(value);
|
|
40
|
+
if (str) {
|
|
41
|
+
return str;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Preserves Cloud Code trace identity for downstream clients by mapping traceId -> responseId.
|
|
49
|
+
*/
|
|
50
|
+
export function injectResponseIdFromTrace<T extends Record<string, unknown>>(body: T): T {
|
|
51
|
+
const traceId = readString(body.traceId);
|
|
52
|
+
if (!traceId) {
|
|
53
|
+
return body;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const response = body.response;
|
|
57
|
+
if (!isRecord(response)) {
|
|
58
|
+
return body;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (readString(response.responseId)) {
|
|
62
|
+
return body;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return {
|
|
66
|
+
...body,
|
|
67
|
+
response: {
|
|
68
|
+
...response,
|
|
69
|
+
responseId: traceId,
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
}
|