opencode-gemini-auth 1.3.10 → 1.3.11
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 +32 -0
- package/package.json +4 -1
- package/src/plugin/auth.test.ts +58 -0
- package/src/plugin/oauth-authorize.ts +198 -0
- package/src/plugin/project/api.ts +209 -0
- package/src/plugin/project/context.ts +187 -0
- package/src/plugin/project/index.ts +6 -0
- package/src/plugin/project/types.ts +55 -0
- package/src/plugin/project/utils.ts +120 -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.ts +102 -588
- package/src/plugin/project.ts +0 -551
- package/src/plugin/request.ts +0 -483
|
@@ -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
|
+
}
|