@ubiquity-os/plugin-sdk 3.8.4 → 3.10.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 +47 -10
- package/dist/configuration.d.mts +61 -29
- package/dist/configuration.d.ts +61 -29
- package/dist/configuration.js +449 -99
- package/dist/configuration.mjs +447 -98
- package/dist/{context-sqbr2o6i.d.mts → context-Dwl3aRX-.d.mts} +29 -0
- package/dist/{context-BbEmsEct.d.ts → context-zLHgu52i.d.ts} +29 -0
- package/dist/index.d.mts +3 -2
- package/dist/index.d.ts +3 -2
- package/dist/index.js +575 -224
- package/dist/index.mjs +574 -223
- package/dist/llm.d.mts +7 -43
- package/dist/llm.d.ts +7 -43
- package/dist/llm.js +176 -44
- package/dist/llm.mjs +176 -44
- package/dist/manifest.d.mts +2 -2
- package/dist/manifest.d.ts +2 -2
- package/dist/signature.js +3 -2
- package/dist/signature.mjs +3 -2
- package/package.json +7 -6
package/dist/llm.d.mts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { ChatCompletionMessageParam, ChatCompletion, ChatCompletionChunk } from 'openai/resources/chat/completions';
|
|
2
|
-
import { C as Context } from './context-
|
|
1
|
+
import { ChatCompletionMessageParam, ChatCompletionCreateParamsNonStreaming, ChatCompletion, ChatCompletionChunk } from 'openai/resources/chat/completions';
|
|
2
|
+
import { C as Context } from './context-Dwl3aRX-.mjs';
|
|
3
|
+
import { PluginInput } from './signature.mjs';
|
|
3
4
|
import '@octokit/webhooks';
|
|
4
5
|
import '@ubiquity-os/ubiquity-os-logger';
|
|
5
6
|
import '@octokit/plugin-rest-endpoint-methods';
|
|
@@ -9,52 +10,15 @@ import '@octokit/plugin-paginate-graphql';
|
|
|
9
10
|
import '@octokit/plugin-paginate-rest';
|
|
10
11
|
import '@octokit/webhooks/node_modules/@octokit/request-error';
|
|
11
12
|
import '@octokit/core';
|
|
13
|
+
import '@sinclair/typebox';
|
|
12
14
|
|
|
13
|
-
type LlmResponseFormat = {
|
|
14
|
-
type: "json_object" | "text";
|
|
15
|
-
} | {
|
|
16
|
-
type: string;
|
|
17
|
-
[key: string]: unknown;
|
|
18
|
-
};
|
|
19
|
-
type LlmPayload = {
|
|
20
|
-
repository?: {
|
|
21
|
-
owner?: {
|
|
22
|
-
login?: string;
|
|
23
|
-
};
|
|
24
|
-
name?: string;
|
|
25
|
-
};
|
|
26
|
-
installation?: {
|
|
27
|
-
id?: number;
|
|
28
|
-
};
|
|
29
|
-
};
|
|
30
|
-
type LlmAuthContext = {
|
|
31
|
-
authToken?: string;
|
|
32
|
-
ubiquityKernelToken?: string;
|
|
33
|
-
payload?: LlmPayload;
|
|
34
|
-
eventPayload?: LlmPayload;
|
|
35
|
-
};
|
|
36
15
|
type LlmCallOptions = {
|
|
37
16
|
baseUrl?: string;
|
|
38
17
|
model?: string;
|
|
39
18
|
stream?: boolean;
|
|
40
19
|
messages: ChatCompletionMessageParam[];
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
top_p?: number;
|
|
45
|
-
frequency_penalty?: number;
|
|
46
|
-
presence_penalty?: number;
|
|
47
|
-
response_format?: LlmResponseFormat;
|
|
48
|
-
stop?: string | string[];
|
|
49
|
-
n?: number;
|
|
50
|
-
logit_bias?: Record<string, number>;
|
|
51
|
-
seed?: number;
|
|
52
|
-
user?: string;
|
|
53
|
-
metadata?: Record<string, unknown>;
|
|
54
|
-
tools?: unknown[];
|
|
55
|
-
tool_choice?: string | Record<string, unknown>;
|
|
56
|
-
[key: string]: unknown;
|
|
57
|
-
};
|
|
58
|
-
declare function callLlm(options: LlmCallOptions, input: Context | LlmAuthContext): Promise<ChatCompletion | AsyncIterable<ChatCompletionChunk>>;
|
|
20
|
+
aiAuthToken?: string;
|
|
21
|
+
} & Partial<Omit<ChatCompletionCreateParamsNonStreaming, "model" | "messages" | "stream">>;
|
|
22
|
+
declare function callLlm(options: LlmCallOptions, input: PluginInput | Context): Promise<ChatCompletion | AsyncIterable<ChatCompletionChunk>>;
|
|
59
23
|
|
|
60
24
|
export { type LlmCallOptions, callLlm };
|
package/dist/llm.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { ChatCompletionMessageParam, ChatCompletion, ChatCompletionChunk } from 'openai/resources/chat/completions';
|
|
2
|
-
import { C as Context } from './context-
|
|
1
|
+
import { ChatCompletionMessageParam, ChatCompletionCreateParamsNonStreaming, ChatCompletion, ChatCompletionChunk } from 'openai/resources/chat/completions';
|
|
2
|
+
import { C as Context } from './context-zLHgu52i.js';
|
|
3
|
+
import { PluginInput } from './signature.js';
|
|
3
4
|
import '@octokit/webhooks';
|
|
4
5
|
import '@ubiquity-os/ubiquity-os-logger';
|
|
5
6
|
import '@octokit/plugin-rest-endpoint-methods';
|
|
@@ -9,52 +10,15 @@ import '@octokit/plugin-paginate-graphql';
|
|
|
9
10
|
import '@octokit/plugin-paginate-rest';
|
|
10
11
|
import '@octokit/webhooks/node_modules/@octokit/request-error';
|
|
11
12
|
import '@octokit/core';
|
|
13
|
+
import '@sinclair/typebox';
|
|
12
14
|
|
|
13
|
-
type LlmResponseFormat = {
|
|
14
|
-
type: "json_object" | "text";
|
|
15
|
-
} | {
|
|
16
|
-
type: string;
|
|
17
|
-
[key: string]: unknown;
|
|
18
|
-
};
|
|
19
|
-
type LlmPayload = {
|
|
20
|
-
repository?: {
|
|
21
|
-
owner?: {
|
|
22
|
-
login?: string;
|
|
23
|
-
};
|
|
24
|
-
name?: string;
|
|
25
|
-
};
|
|
26
|
-
installation?: {
|
|
27
|
-
id?: number;
|
|
28
|
-
};
|
|
29
|
-
};
|
|
30
|
-
type LlmAuthContext = {
|
|
31
|
-
authToken?: string;
|
|
32
|
-
ubiquityKernelToken?: string;
|
|
33
|
-
payload?: LlmPayload;
|
|
34
|
-
eventPayload?: LlmPayload;
|
|
35
|
-
};
|
|
36
15
|
type LlmCallOptions = {
|
|
37
16
|
baseUrl?: string;
|
|
38
17
|
model?: string;
|
|
39
18
|
stream?: boolean;
|
|
40
19
|
messages: ChatCompletionMessageParam[];
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
top_p?: number;
|
|
45
|
-
frequency_penalty?: number;
|
|
46
|
-
presence_penalty?: number;
|
|
47
|
-
response_format?: LlmResponseFormat;
|
|
48
|
-
stop?: string | string[];
|
|
49
|
-
n?: number;
|
|
50
|
-
logit_bias?: Record<string, number>;
|
|
51
|
-
seed?: number;
|
|
52
|
-
user?: string;
|
|
53
|
-
metadata?: Record<string, unknown>;
|
|
54
|
-
tools?: unknown[];
|
|
55
|
-
tool_choice?: string | Record<string, unknown>;
|
|
56
|
-
[key: string]: unknown;
|
|
57
|
-
};
|
|
58
|
-
declare function callLlm(options: LlmCallOptions, input: Context | LlmAuthContext): Promise<ChatCompletion | AsyncIterable<ChatCompletionChunk>>;
|
|
20
|
+
aiAuthToken?: string;
|
|
21
|
+
} & Partial<Omit<ChatCompletionCreateParamsNonStreaming, "model" | "messages" | "stream">>;
|
|
22
|
+
declare function callLlm(options: LlmCallOptions, input: PluginInput | Context): Promise<ChatCompletion | AsyncIterable<ChatCompletionChunk>>;
|
|
59
23
|
|
|
60
24
|
export { type LlmCallOptions, callLlm };
|
package/dist/llm.js
CHANGED
|
@@ -23,6 +23,7 @@ __export(llm_exports, {
|
|
|
23
23
|
callLlm: () => callLlm
|
|
24
24
|
});
|
|
25
25
|
module.exports = __toCommonJS(llm_exports);
|
|
26
|
+
var EMPTY_STRING = "";
|
|
26
27
|
function normalizeBaseUrl(baseUrl) {
|
|
27
28
|
let normalized = baseUrl.trim();
|
|
28
29
|
while (normalized.endsWith("/")) {
|
|
@@ -30,87 +31,218 @@ function normalizeBaseUrl(baseUrl) {
|
|
|
30
31
|
}
|
|
31
32
|
return normalized;
|
|
32
33
|
}
|
|
34
|
+
var MAX_LLM_RETRIES = 2;
|
|
35
|
+
var RETRY_BACKOFF_MS = [250, 750];
|
|
36
|
+
function getRetryDelayMs(attempt) {
|
|
37
|
+
return RETRY_BACKOFF_MS[Math.min(attempt, RETRY_BACKOFF_MS.length - 1)] ?? 750;
|
|
38
|
+
}
|
|
39
|
+
function sleep(ms) {
|
|
40
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
41
|
+
}
|
|
33
42
|
function getEnvString(name) {
|
|
34
|
-
if (typeof process === "undefined" || !process?.env) return
|
|
35
|
-
return String(process.env[name] ??
|
|
43
|
+
if (typeof process === "undefined" || !process?.env) return EMPTY_STRING;
|
|
44
|
+
return String(process.env[name] ?? EMPTY_STRING).trim();
|
|
45
|
+
}
|
|
46
|
+
function normalizeToken(value) {
|
|
47
|
+
return typeof value === "string" ? value.trim() : EMPTY_STRING;
|
|
48
|
+
}
|
|
49
|
+
function isGitHubToken(token) {
|
|
50
|
+
return token.trim().startsWith("gh");
|
|
51
|
+
}
|
|
52
|
+
function getEnvTokenFromInput(input) {
|
|
53
|
+
if ("env" in input) {
|
|
54
|
+
const envValue = input.env;
|
|
55
|
+
if (envValue && typeof envValue === "object") {
|
|
56
|
+
const token = normalizeToken(envValue.UOS_AI_TOKEN);
|
|
57
|
+
if (token) return token;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return getEnvString("UOS_AI_TOKEN");
|
|
61
|
+
}
|
|
62
|
+
function resolveAuthToken(input, aiAuthToken) {
|
|
63
|
+
const explicit = normalizeToken(aiAuthToken);
|
|
64
|
+
if (explicit) return { token: explicit, isGitHub: isGitHubToken(explicit) };
|
|
65
|
+
const envToken = getEnvTokenFromInput(input);
|
|
66
|
+
if (envToken) return { token: envToken, isGitHub: isGitHubToken(envToken) };
|
|
67
|
+
const fallback = normalizeToken(input.authToken);
|
|
68
|
+
if (!fallback) {
|
|
69
|
+
const err = new Error("Missing auth token; set UOS_AI_TOKEN, pass aiAuthToken, or provide authToken in input");
|
|
70
|
+
err.status = 401;
|
|
71
|
+
throw err;
|
|
72
|
+
}
|
|
73
|
+
return { token: fallback, isGitHub: isGitHubToken(fallback) };
|
|
36
74
|
}
|
|
37
75
|
function getAiBaseUrl(options) {
|
|
38
76
|
if (typeof options.baseUrl === "string" && options.baseUrl.trim()) {
|
|
39
77
|
return normalizeBaseUrl(options.baseUrl);
|
|
40
78
|
}
|
|
41
|
-
const envBaseUrl = getEnvString("
|
|
79
|
+
const envBaseUrl = getEnvString("UOS_AI_URL");
|
|
42
80
|
if (envBaseUrl) return normalizeBaseUrl(envBaseUrl);
|
|
43
|
-
return "https://ai
|
|
81
|
+
return "https://ai-ubq-fi.deno.dev";
|
|
44
82
|
}
|
|
45
83
|
async function callLlm(options, input) {
|
|
46
|
-
const
|
|
47
|
-
const authToken =
|
|
48
|
-
const
|
|
49
|
-
const payload =
|
|
50
|
-
const owner = payload
|
|
51
|
-
|
|
52
|
-
const
|
|
53
|
-
if (!authToken) throw new Error("Missing authToken in inputs");
|
|
54
|
-
const isKernelTokenRequired = authToken.trim().startsWith("gh");
|
|
55
|
-
if (isKernelTokenRequired && !ubiquityKernelToken) {
|
|
56
|
-
throw new Error("Missing ubiquityKernelToken in inputs (kernel attestation is required for GitHub auth)");
|
|
57
|
-
}
|
|
58
|
-
const { baseUrl, model, stream: isStream, messages, ...rest } = options;
|
|
59
|
-
const url = `${getAiBaseUrl({ ...options, baseUrl })}/v1/chat/completions`;
|
|
84
|
+
const { baseUrl, model, stream: isStream, messages, aiAuthToken, ...rest } = options;
|
|
85
|
+
const { token: authToken, isGitHub } = resolveAuthToken(input, aiAuthToken);
|
|
86
|
+
const kernelToken = "ubiquityKernelToken" in input ? input.ubiquityKernelToken : void 0;
|
|
87
|
+
const payload = getPayload(input);
|
|
88
|
+
const { owner, repo, installationId } = getRepoMetadata(payload);
|
|
89
|
+
ensureMessages(messages);
|
|
90
|
+
const url = buildAiUrl(options, baseUrl);
|
|
60
91
|
const body = JSON.stringify({
|
|
61
92
|
...rest,
|
|
62
93
|
...model ? { model } : {},
|
|
63
94
|
messages,
|
|
64
95
|
stream: isStream ?? false
|
|
65
96
|
});
|
|
66
|
-
const headers = {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
headers["X-GitHub-Installation-Id"] = String(installationId);
|
|
74
|
-
}
|
|
75
|
-
if (ubiquityKernelToken) {
|
|
76
|
-
headers["X-Ubiquity-Kernel-Token"] = ubiquityKernelToken;
|
|
77
|
-
}
|
|
78
|
-
const response = await fetch(url, { method: "POST", headers, body });
|
|
79
|
-
if (!response.ok) {
|
|
80
|
-
const err = await response.text();
|
|
81
|
-
throw new Error(`LLM API error: ${response.status} - ${err}`);
|
|
82
|
-
}
|
|
97
|
+
const headers = buildHeaders(authToken, {
|
|
98
|
+
owner,
|
|
99
|
+
repo,
|
|
100
|
+
installationId,
|
|
101
|
+
ubiquityKernelToken: isGitHub ? kernelToken : void 0
|
|
102
|
+
});
|
|
103
|
+
const response = await fetchWithRetry(url, { method: "POST", headers, body }, MAX_LLM_RETRIES);
|
|
83
104
|
if (isStream) {
|
|
84
105
|
if (!response.body) {
|
|
85
106
|
throw new Error("LLM API error: missing response body for streaming request");
|
|
86
107
|
}
|
|
87
108
|
return parseSseStream(response.body);
|
|
88
109
|
}
|
|
89
|
-
|
|
110
|
+
const rawText = await response.text();
|
|
111
|
+
try {
|
|
112
|
+
return JSON.parse(rawText);
|
|
113
|
+
} catch (err) {
|
|
114
|
+
const preview = rawText ? rawText.slice(0, 1e3) : EMPTY_STRING;
|
|
115
|
+
const message = "LLM API error: failed to parse JSON response from server" + (preview ? `; response body (truncated): ${preview}` : EMPTY_STRING);
|
|
116
|
+
const error = new Error(message);
|
|
117
|
+
error.cause = err;
|
|
118
|
+
error.status = response.status;
|
|
119
|
+
throw error;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
function ensureMessages(messages) {
|
|
123
|
+
if (!Array.isArray(messages) || messages.length === 0) {
|
|
124
|
+
const err = new Error("messages must be a non-empty array");
|
|
125
|
+
err.status = 400;
|
|
126
|
+
throw err;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
function buildAiUrl(options, baseUrl) {
|
|
130
|
+
return `${getAiBaseUrl({ ...options, baseUrl })}/v1/chat/completions`;
|
|
131
|
+
}
|
|
132
|
+
async function fetchWithRetry(url, options, maxRetries) {
|
|
133
|
+
let attempt = 0;
|
|
134
|
+
let lastError;
|
|
135
|
+
while (attempt <= maxRetries) {
|
|
136
|
+
try {
|
|
137
|
+
const response = await fetch(url, options);
|
|
138
|
+
if (response.ok) return response;
|
|
139
|
+
throw await buildResponseError(response);
|
|
140
|
+
} catch (error) {
|
|
141
|
+
lastError = error;
|
|
142
|
+
if (!shouldRetryError(error, attempt, maxRetries)) throw error;
|
|
143
|
+
await sleep(getRetryDelayMs(attempt));
|
|
144
|
+
attempt += 1;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
throw lastError ?? new Error("LLM API error: request failed after retries");
|
|
148
|
+
}
|
|
149
|
+
async function buildResponseError(response) {
|
|
150
|
+
const errText = await response.text();
|
|
151
|
+
const error = new Error(`LLM API error: ${response.status} - ${errText}`);
|
|
152
|
+
error.status = response.status;
|
|
153
|
+
return error;
|
|
154
|
+
}
|
|
155
|
+
function shouldRetryError(error, attempt, maxRetries) {
|
|
156
|
+
if (attempt >= maxRetries) return false;
|
|
157
|
+
const status = getErrorStatus(error);
|
|
158
|
+
if (typeof status === "number") {
|
|
159
|
+
return status >= 500;
|
|
160
|
+
}
|
|
161
|
+
return true;
|
|
162
|
+
}
|
|
163
|
+
function getErrorStatus(error) {
|
|
164
|
+
return typeof error?.status === "number" ? error.status : void 0;
|
|
165
|
+
}
|
|
166
|
+
function getPayload(input) {
|
|
167
|
+
if ("payload" in input) {
|
|
168
|
+
return input.payload;
|
|
169
|
+
}
|
|
170
|
+
return input.eventPayload;
|
|
171
|
+
}
|
|
172
|
+
function getRepoMetadata(payload) {
|
|
173
|
+
const repoPayload = payload;
|
|
174
|
+
return {
|
|
175
|
+
owner: repoPayload?.repository?.owner?.login ?? EMPTY_STRING,
|
|
176
|
+
repo: repoPayload?.repository?.name ?? EMPTY_STRING,
|
|
177
|
+
installationId: repoPayload?.installation?.id
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
function buildHeaders(authToken, options) {
|
|
181
|
+
const headers = {
|
|
182
|
+
Authorization: `Bearer ${authToken}`,
|
|
183
|
+
"Content-Type": "application/json"
|
|
184
|
+
};
|
|
185
|
+
if (options.owner) headers["X-GitHub-Owner"] = options.owner;
|
|
186
|
+
if (options.repo) headers["X-GitHub-Repo"] = options.repo;
|
|
187
|
+
if (typeof options.installationId === "number" && Number.isFinite(options.installationId)) {
|
|
188
|
+
headers["X-GitHub-Installation-Id"] = String(options.installationId);
|
|
189
|
+
}
|
|
190
|
+
if (options.ubiquityKernelToken) {
|
|
191
|
+
headers["X-Ubiquity-Kernel-Token"] = options.ubiquityKernelToken;
|
|
192
|
+
}
|
|
193
|
+
return headers;
|
|
90
194
|
}
|
|
91
195
|
async function* parseSseStream(body) {
|
|
92
196
|
const reader = body.getReader();
|
|
93
197
|
const decoder = new TextDecoder();
|
|
94
|
-
let buffer =
|
|
198
|
+
let buffer = EMPTY_STRING;
|
|
95
199
|
try {
|
|
96
200
|
while (true) {
|
|
97
201
|
const { value, done: isDone } = await reader.read();
|
|
98
202
|
if (isDone) break;
|
|
99
203
|
buffer += decoder.decode(value, { stream: true });
|
|
100
|
-
const events = buffer
|
|
101
|
-
buffer =
|
|
204
|
+
const { events, remainder } = splitSseEvents(buffer);
|
|
205
|
+
buffer = remainder;
|
|
102
206
|
for (const event of events) {
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
}
|
|
207
|
+
const data = getEventData(event);
|
|
208
|
+
if (!data) continue;
|
|
209
|
+
if (data.trim() === "[DONE]") return;
|
|
210
|
+
yield parseEventData(data);
|
|
108
211
|
}
|
|
109
212
|
}
|
|
110
213
|
} finally {
|
|
111
214
|
reader.releaseLock();
|
|
112
215
|
}
|
|
113
216
|
}
|
|
217
|
+
function splitSseEvents(buffer) {
|
|
218
|
+
const normalized = buffer.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
219
|
+
const parts = normalized.split("\n\n");
|
|
220
|
+
const remainder = parts.pop() ?? EMPTY_STRING;
|
|
221
|
+
return { events: parts, remainder };
|
|
222
|
+
}
|
|
223
|
+
function getEventData(event) {
|
|
224
|
+
if (!event.trim()) return null;
|
|
225
|
+
const dataLines = event.split("\n").filter((line) => line.startsWith("data:"));
|
|
226
|
+
if (!dataLines.length) return null;
|
|
227
|
+
const data = dataLines.map((line) => line.startsWith("data: ") ? line.slice(6) : line.slice(5).replace(/^ /, EMPTY_STRING)).join("\n");
|
|
228
|
+
return data || null;
|
|
229
|
+
}
|
|
230
|
+
function parseEventData(data) {
|
|
231
|
+
try {
|
|
232
|
+
return JSON.parse(data);
|
|
233
|
+
} catch (error) {
|
|
234
|
+
if (data.includes("\n")) {
|
|
235
|
+
const collapsed = data.replace(/\n/g, EMPTY_STRING);
|
|
236
|
+
try {
|
|
237
|
+
return JSON.parse(collapsed);
|
|
238
|
+
} catch {
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
242
|
+
const preview = data.length > 200 ? `${data.slice(0, 200)}...` : data;
|
|
243
|
+
throw new Error(`LLM stream parse error: ${message}. Data: ${preview}`);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
114
246
|
// Annotate the CommonJS export names for ESM import in node:
|
|
115
247
|
0 && (module.exports = {
|
|
116
248
|
callLlm
|
package/dist/llm.mjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
// src/llm/index.ts
|
|
2
|
+
var EMPTY_STRING = "";
|
|
2
3
|
function normalizeBaseUrl(baseUrl) {
|
|
3
4
|
let normalized = baseUrl.trim();
|
|
4
5
|
while (normalized.endsWith("/")) {
|
|
@@ -6,87 +7,218 @@ function normalizeBaseUrl(baseUrl) {
|
|
|
6
7
|
}
|
|
7
8
|
return normalized;
|
|
8
9
|
}
|
|
10
|
+
var MAX_LLM_RETRIES = 2;
|
|
11
|
+
var RETRY_BACKOFF_MS = [250, 750];
|
|
12
|
+
function getRetryDelayMs(attempt) {
|
|
13
|
+
return RETRY_BACKOFF_MS[Math.min(attempt, RETRY_BACKOFF_MS.length - 1)] ?? 750;
|
|
14
|
+
}
|
|
15
|
+
function sleep(ms) {
|
|
16
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
17
|
+
}
|
|
9
18
|
function getEnvString(name) {
|
|
10
|
-
if (typeof process === "undefined" || !process?.env) return
|
|
11
|
-
return String(process.env[name] ??
|
|
19
|
+
if (typeof process === "undefined" || !process?.env) return EMPTY_STRING;
|
|
20
|
+
return String(process.env[name] ?? EMPTY_STRING).trim();
|
|
21
|
+
}
|
|
22
|
+
function normalizeToken(value) {
|
|
23
|
+
return typeof value === "string" ? value.trim() : EMPTY_STRING;
|
|
24
|
+
}
|
|
25
|
+
function isGitHubToken(token) {
|
|
26
|
+
return token.trim().startsWith("gh");
|
|
27
|
+
}
|
|
28
|
+
function getEnvTokenFromInput(input) {
|
|
29
|
+
if ("env" in input) {
|
|
30
|
+
const envValue = input.env;
|
|
31
|
+
if (envValue && typeof envValue === "object") {
|
|
32
|
+
const token = normalizeToken(envValue.UOS_AI_TOKEN);
|
|
33
|
+
if (token) return token;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return getEnvString("UOS_AI_TOKEN");
|
|
37
|
+
}
|
|
38
|
+
function resolveAuthToken(input, aiAuthToken) {
|
|
39
|
+
const explicit = normalizeToken(aiAuthToken);
|
|
40
|
+
if (explicit) return { token: explicit, isGitHub: isGitHubToken(explicit) };
|
|
41
|
+
const envToken = getEnvTokenFromInput(input);
|
|
42
|
+
if (envToken) return { token: envToken, isGitHub: isGitHubToken(envToken) };
|
|
43
|
+
const fallback = normalizeToken(input.authToken);
|
|
44
|
+
if (!fallback) {
|
|
45
|
+
const err = new Error("Missing auth token; set UOS_AI_TOKEN, pass aiAuthToken, or provide authToken in input");
|
|
46
|
+
err.status = 401;
|
|
47
|
+
throw err;
|
|
48
|
+
}
|
|
49
|
+
return { token: fallback, isGitHub: isGitHubToken(fallback) };
|
|
12
50
|
}
|
|
13
51
|
function getAiBaseUrl(options) {
|
|
14
52
|
if (typeof options.baseUrl === "string" && options.baseUrl.trim()) {
|
|
15
53
|
return normalizeBaseUrl(options.baseUrl);
|
|
16
54
|
}
|
|
17
|
-
const envBaseUrl = getEnvString("
|
|
55
|
+
const envBaseUrl = getEnvString("UOS_AI_URL");
|
|
18
56
|
if (envBaseUrl) return normalizeBaseUrl(envBaseUrl);
|
|
19
|
-
return "https://ai
|
|
57
|
+
return "https://ai-ubq-fi.deno.dev";
|
|
20
58
|
}
|
|
21
59
|
async function callLlm(options, input) {
|
|
22
|
-
const
|
|
23
|
-
const authToken =
|
|
24
|
-
const
|
|
25
|
-
const payload =
|
|
26
|
-
const owner = payload
|
|
27
|
-
|
|
28
|
-
const
|
|
29
|
-
if (!authToken) throw new Error("Missing authToken in inputs");
|
|
30
|
-
const isKernelTokenRequired = authToken.trim().startsWith("gh");
|
|
31
|
-
if (isKernelTokenRequired && !ubiquityKernelToken) {
|
|
32
|
-
throw new Error("Missing ubiquityKernelToken in inputs (kernel attestation is required for GitHub auth)");
|
|
33
|
-
}
|
|
34
|
-
const { baseUrl, model, stream: isStream, messages, ...rest } = options;
|
|
35
|
-
const url = `${getAiBaseUrl({ ...options, baseUrl })}/v1/chat/completions`;
|
|
60
|
+
const { baseUrl, model, stream: isStream, messages, aiAuthToken, ...rest } = options;
|
|
61
|
+
const { token: authToken, isGitHub } = resolveAuthToken(input, aiAuthToken);
|
|
62
|
+
const kernelToken = "ubiquityKernelToken" in input ? input.ubiquityKernelToken : void 0;
|
|
63
|
+
const payload = getPayload(input);
|
|
64
|
+
const { owner, repo, installationId } = getRepoMetadata(payload);
|
|
65
|
+
ensureMessages(messages);
|
|
66
|
+
const url = buildAiUrl(options, baseUrl);
|
|
36
67
|
const body = JSON.stringify({
|
|
37
68
|
...rest,
|
|
38
69
|
...model ? { model } : {},
|
|
39
70
|
messages,
|
|
40
71
|
stream: isStream ?? false
|
|
41
72
|
});
|
|
42
|
-
const headers = {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
headers["X-GitHub-Installation-Id"] = String(installationId);
|
|
50
|
-
}
|
|
51
|
-
if (ubiquityKernelToken) {
|
|
52
|
-
headers["X-Ubiquity-Kernel-Token"] = ubiquityKernelToken;
|
|
53
|
-
}
|
|
54
|
-
const response = await fetch(url, { method: "POST", headers, body });
|
|
55
|
-
if (!response.ok) {
|
|
56
|
-
const err = await response.text();
|
|
57
|
-
throw new Error(`LLM API error: ${response.status} - ${err}`);
|
|
58
|
-
}
|
|
73
|
+
const headers = buildHeaders(authToken, {
|
|
74
|
+
owner,
|
|
75
|
+
repo,
|
|
76
|
+
installationId,
|
|
77
|
+
ubiquityKernelToken: isGitHub ? kernelToken : void 0
|
|
78
|
+
});
|
|
79
|
+
const response = await fetchWithRetry(url, { method: "POST", headers, body }, MAX_LLM_RETRIES);
|
|
59
80
|
if (isStream) {
|
|
60
81
|
if (!response.body) {
|
|
61
82
|
throw new Error("LLM API error: missing response body for streaming request");
|
|
62
83
|
}
|
|
63
84
|
return parseSseStream(response.body);
|
|
64
85
|
}
|
|
65
|
-
|
|
86
|
+
const rawText = await response.text();
|
|
87
|
+
try {
|
|
88
|
+
return JSON.parse(rawText);
|
|
89
|
+
} catch (err) {
|
|
90
|
+
const preview = rawText ? rawText.slice(0, 1e3) : EMPTY_STRING;
|
|
91
|
+
const message = "LLM API error: failed to parse JSON response from server" + (preview ? `; response body (truncated): ${preview}` : EMPTY_STRING);
|
|
92
|
+
const error = new Error(message);
|
|
93
|
+
error.cause = err;
|
|
94
|
+
error.status = response.status;
|
|
95
|
+
throw error;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
function ensureMessages(messages) {
|
|
99
|
+
if (!Array.isArray(messages) || messages.length === 0) {
|
|
100
|
+
const err = new Error("messages must be a non-empty array");
|
|
101
|
+
err.status = 400;
|
|
102
|
+
throw err;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
function buildAiUrl(options, baseUrl) {
|
|
106
|
+
return `${getAiBaseUrl({ ...options, baseUrl })}/v1/chat/completions`;
|
|
107
|
+
}
|
|
108
|
+
async function fetchWithRetry(url, options, maxRetries) {
|
|
109
|
+
let attempt = 0;
|
|
110
|
+
let lastError;
|
|
111
|
+
while (attempt <= maxRetries) {
|
|
112
|
+
try {
|
|
113
|
+
const response = await fetch(url, options);
|
|
114
|
+
if (response.ok) return response;
|
|
115
|
+
throw await buildResponseError(response);
|
|
116
|
+
} catch (error) {
|
|
117
|
+
lastError = error;
|
|
118
|
+
if (!shouldRetryError(error, attempt, maxRetries)) throw error;
|
|
119
|
+
await sleep(getRetryDelayMs(attempt));
|
|
120
|
+
attempt += 1;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
throw lastError ?? new Error("LLM API error: request failed after retries");
|
|
124
|
+
}
|
|
125
|
+
async function buildResponseError(response) {
|
|
126
|
+
const errText = await response.text();
|
|
127
|
+
const error = new Error(`LLM API error: ${response.status} - ${errText}`);
|
|
128
|
+
error.status = response.status;
|
|
129
|
+
return error;
|
|
130
|
+
}
|
|
131
|
+
function shouldRetryError(error, attempt, maxRetries) {
|
|
132
|
+
if (attempt >= maxRetries) return false;
|
|
133
|
+
const status = getErrorStatus(error);
|
|
134
|
+
if (typeof status === "number") {
|
|
135
|
+
return status >= 500;
|
|
136
|
+
}
|
|
137
|
+
return true;
|
|
138
|
+
}
|
|
139
|
+
function getErrorStatus(error) {
|
|
140
|
+
return typeof error?.status === "number" ? error.status : void 0;
|
|
141
|
+
}
|
|
142
|
+
function getPayload(input) {
|
|
143
|
+
if ("payload" in input) {
|
|
144
|
+
return input.payload;
|
|
145
|
+
}
|
|
146
|
+
return input.eventPayload;
|
|
147
|
+
}
|
|
148
|
+
function getRepoMetadata(payload) {
|
|
149
|
+
const repoPayload = payload;
|
|
150
|
+
return {
|
|
151
|
+
owner: repoPayload?.repository?.owner?.login ?? EMPTY_STRING,
|
|
152
|
+
repo: repoPayload?.repository?.name ?? EMPTY_STRING,
|
|
153
|
+
installationId: repoPayload?.installation?.id
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
function buildHeaders(authToken, options) {
|
|
157
|
+
const headers = {
|
|
158
|
+
Authorization: `Bearer ${authToken}`,
|
|
159
|
+
"Content-Type": "application/json"
|
|
160
|
+
};
|
|
161
|
+
if (options.owner) headers["X-GitHub-Owner"] = options.owner;
|
|
162
|
+
if (options.repo) headers["X-GitHub-Repo"] = options.repo;
|
|
163
|
+
if (typeof options.installationId === "number" && Number.isFinite(options.installationId)) {
|
|
164
|
+
headers["X-GitHub-Installation-Id"] = String(options.installationId);
|
|
165
|
+
}
|
|
166
|
+
if (options.ubiquityKernelToken) {
|
|
167
|
+
headers["X-Ubiquity-Kernel-Token"] = options.ubiquityKernelToken;
|
|
168
|
+
}
|
|
169
|
+
return headers;
|
|
66
170
|
}
|
|
67
171
|
async function* parseSseStream(body) {
|
|
68
172
|
const reader = body.getReader();
|
|
69
173
|
const decoder = new TextDecoder();
|
|
70
|
-
let buffer =
|
|
174
|
+
let buffer = EMPTY_STRING;
|
|
71
175
|
try {
|
|
72
176
|
while (true) {
|
|
73
177
|
const { value, done: isDone } = await reader.read();
|
|
74
178
|
if (isDone) break;
|
|
75
179
|
buffer += decoder.decode(value, { stream: true });
|
|
76
|
-
const events = buffer
|
|
77
|
-
buffer =
|
|
180
|
+
const { events, remainder } = splitSseEvents(buffer);
|
|
181
|
+
buffer = remainder;
|
|
78
182
|
for (const event of events) {
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
}
|
|
183
|
+
const data = getEventData(event);
|
|
184
|
+
if (!data) continue;
|
|
185
|
+
if (data.trim() === "[DONE]") return;
|
|
186
|
+
yield parseEventData(data);
|
|
84
187
|
}
|
|
85
188
|
}
|
|
86
189
|
} finally {
|
|
87
190
|
reader.releaseLock();
|
|
88
191
|
}
|
|
89
192
|
}
|
|
193
|
+
function splitSseEvents(buffer) {
|
|
194
|
+
const normalized = buffer.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
195
|
+
const parts = normalized.split("\n\n");
|
|
196
|
+
const remainder = parts.pop() ?? EMPTY_STRING;
|
|
197
|
+
return { events: parts, remainder };
|
|
198
|
+
}
|
|
199
|
+
function getEventData(event) {
|
|
200
|
+
if (!event.trim()) return null;
|
|
201
|
+
const dataLines = event.split("\n").filter((line) => line.startsWith("data:"));
|
|
202
|
+
if (!dataLines.length) return null;
|
|
203
|
+
const data = dataLines.map((line) => line.startsWith("data: ") ? line.slice(6) : line.slice(5).replace(/^ /, EMPTY_STRING)).join("\n");
|
|
204
|
+
return data || null;
|
|
205
|
+
}
|
|
206
|
+
function parseEventData(data) {
|
|
207
|
+
try {
|
|
208
|
+
return JSON.parse(data);
|
|
209
|
+
} catch (error) {
|
|
210
|
+
if (data.includes("\n")) {
|
|
211
|
+
const collapsed = data.replace(/\n/g, EMPTY_STRING);
|
|
212
|
+
try {
|
|
213
|
+
return JSON.parse(collapsed);
|
|
214
|
+
} catch {
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
218
|
+
const preview = data.length > 200 ? `${data.slice(0, 200)}...` : data;
|
|
219
|
+
throw new Error(`LLM stream parse error: ${message}. Data: ${preview}`);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
90
222
|
export {
|
|
91
223
|
callLlm
|
|
92
224
|
};
|