dsh-provider-qoder 0.1.0 → 0.2.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 +10 -5
- package/README_CN.md +10 -5
- package/lib/client.js +189 -84
- package/lib/index.d.ts +60 -341
- package/lib/index.js +1666 -831
- package/package.json +3 -1
package/lib/index.js
CHANGED
|
@@ -1,23 +1,203 @@
|
|
|
1
1
|
import z from "@deepseek-ai/schemastery";
|
|
2
|
-
import { EMPTY_RESPONSE_CODE, LlmAdapter, LlmError, ReasoningEffortId, ToolCallId, attributionHeaders } from "@deepseek-ai/dsh-llm";
|
|
3
|
-
import crypto from "node:crypto";
|
|
2
|
+
import { EMPTY_RESPONSE_CODE, LlmAdapter, LlmError, ProviderRequestId, ReasoningEffortId, ToolCallId, attributionHeaders, contentHasImage } from "@deepseek-ai/dsh-llm";
|
|
3
|
+
import crypto, { createHash } from "node:crypto";
|
|
4
4
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
5
5
|
import { homedir } from "node:os";
|
|
6
6
|
import { dirname, join } from "node:path";
|
|
7
7
|
import { credentialRef } from "@deepseek-ai/dsh-credentials";
|
|
8
|
-
//#region src/
|
|
8
|
+
//#region src/qoder/catalog.ts
|
|
9
|
+
const discoveredMetadataKeys = [
|
|
10
|
+
"description",
|
|
11
|
+
"source",
|
|
12
|
+
"isReasoning",
|
|
13
|
+
"supportsEffort",
|
|
14
|
+
"reasoningEfforts",
|
|
15
|
+
"defaultReasoningEffort",
|
|
16
|
+
"priceFactor",
|
|
17
|
+
"contextOptions",
|
|
18
|
+
"maxContextWindow",
|
|
19
|
+
"supportsImages"
|
|
20
|
+
];
|
|
21
|
+
const reasoningEffortOrder = /* @__PURE__ */ new Map([
|
|
22
|
+
["low", 0],
|
|
23
|
+
["medium", 1],
|
|
24
|
+
["high", 2],
|
|
25
|
+
["xhigh", 3],
|
|
26
|
+
["max", 4]
|
|
27
|
+
]);
|
|
28
|
+
function positiveNumber(value) {
|
|
29
|
+
return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : void 0;
|
|
30
|
+
}
|
|
31
|
+
function contextOptionsOf(value) {
|
|
32
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return void 0;
|
|
33
|
+
const options = {};
|
|
34
|
+
for (const [key, raw] of Object.entries(value)) {
|
|
35
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) continue;
|
|
36
|
+
const entry = raw;
|
|
37
|
+
const tokenCount = positiveNumber(entry.token_count);
|
|
38
|
+
const isDefault = typeof entry.is_default === "boolean" ? entry.is_default : void 0;
|
|
39
|
+
if (tokenCount === void 0) continue;
|
|
40
|
+
options[key] = {
|
|
41
|
+
...tokenCount === void 0 ? {} : { tokenCount },
|
|
42
|
+
...isDefault === void 0 ? {} : { isDefault }
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
if (Object.keys(options).length === 0) return void 0;
|
|
46
|
+
return options;
|
|
47
|
+
}
|
|
48
|
+
function reasoningEffortsOf(value) {
|
|
49
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return {};
|
|
50
|
+
const enabled = value.enabled;
|
|
51
|
+
if (typeof enabled !== "object" || enabled === null || Array.isArray(enabled)) return {};
|
|
52
|
+
const rawEfforts = enabled.efforts;
|
|
53
|
+
if (typeof rawEfforts !== "object" || rawEfforts === null || Array.isArray(rawEfforts)) return {};
|
|
54
|
+
const efforts = [];
|
|
55
|
+
let defaultEffort;
|
|
56
|
+
for (const [id, raw] of Object.entries(rawEfforts)) {
|
|
57
|
+
if (!id.trim() || typeof raw !== "object" || raw === null || Array.isArray(raw)) continue;
|
|
58
|
+
const entry = raw;
|
|
59
|
+
efforts.push({
|
|
60
|
+
id,
|
|
61
|
+
name: id,
|
|
62
|
+
...typeof entry.description === "string" && entry.description.trim() ? { description: entry.description.trim() } : {}
|
|
63
|
+
});
|
|
64
|
+
if (entry.is_default === true) defaultEffort = id;
|
|
65
|
+
}
|
|
66
|
+
return {
|
|
67
|
+
...efforts.length === 0 ? {} : { efforts: efforts.sort((left, right) => (reasoningEffortOrder.get(left.id) ?? Number.MAX_SAFE_INTEGER) - (reasoningEffortOrder.get(right.id) ?? Number.MAX_SAFE_INTEGER)) },
|
|
68
|
+
...defaultEffort === void 0 ? {} : { defaultEffort }
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
function thinkingDefault(value, fallback, onConflict) {
|
|
72
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return fallback;
|
|
73
|
+
const config = value;
|
|
74
|
+
const isDefault = (entry) => typeof entry === "object" && entry !== null && !Array.isArray(entry) && entry.is_default === true;
|
|
75
|
+
const enabled = isDefault(config.enabled);
|
|
76
|
+
const disabled = isDefault(config.disabled);
|
|
77
|
+
if (enabled && disabled) onConflict?.("thinking-defaults");
|
|
78
|
+
return enabled === disabled ? fallback : enabled;
|
|
79
|
+
}
|
|
80
|
+
function normalizeQoderModels(payload, onConflict) {
|
|
81
|
+
if (typeof payload !== "object" || payload === null || !Array.isArray(payload.assistant)) return [];
|
|
82
|
+
const models = [];
|
|
83
|
+
const seen = /* @__PURE__ */ new Set();
|
|
84
|
+
for (const raw of payload.assistant) {
|
|
85
|
+
if (typeof raw !== "object" || raw === null || raw.enable !== true) continue;
|
|
86
|
+
const id = typeof raw.key === "string" ? raw.key.trim() : "";
|
|
87
|
+
if (!id || seen.has(id)) continue;
|
|
88
|
+
seen.add(id);
|
|
89
|
+
const contextOptions = contextOptionsOf(raw.context_config);
|
|
90
|
+
const defaultOptions = Object.values(contextOptions ?? {}).filter((option) => option.isDefault && option.tokenCount !== void 0);
|
|
91
|
+
if (defaultOptions.length > 1) onConflict?.("context-defaults");
|
|
92
|
+
const contextWindow = (defaultOptions.length === 1 ? defaultOptions[0].tokenCount : void 0) ?? positiveNumber(raw.max_input_tokens) ?? 18e4;
|
|
93
|
+
const maxContextWindow = Math.max(positiveNumber(raw.max_input_tokens) ?? 0, contextWindow, ...Object.values(contextOptions ?? {}).map((option) => option.tokenCount ?? 0));
|
|
94
|
+
const isReasoning = thinkingDefault(raw.thinking_config, raw.is_reasoning === true, onConflict);
|
|
95
|
+
const priceFactor = typeof raw.price_factor === "number" && Number.isFinite(raw.price_factor) && raw.price_factor >= 0 ? raw.price_factor : void 0;
|
|
96
|
+
const reasoning = reasoningEffortsOf(raw.thinking_config);
|
|
97
|
+
models.push({
|
|
98
|
+
id,
|
|
99
|
+
name: typeof raw.display_name === "string" && raw.display_name.trim() ? raw.display_name.trim() : id,
|
|
100
|
+
contextWindow,
|
|
101
|
+
maxContextWindow,
|
|
102
|
+
maxTokens: positiveNumber(raw.max_output_tokens) ?? 32768,
|
|
103
|
+
source: typeof raw.source === "string" && raw.source.trim() ? raw.source.trim() : "system",
|
|
104
|
+
isReasoning,
|
|
105
|
+
supportsEffort: reasoning.efforts !== void 0,
|
|
106
|
+
supportsImages: raw.is_vl === true,
|
|
107
|
+
...reasoning.efforts === void 0 ? {} : { reasoningEfforts: reasoning.efforts },
|
|
108
|
+
...reasoning.defaultEffort === void 0 ? {} : { defaultReasoningEffort: reasoning.defaultEffort },
|
|
109
|
+
...priceFactor === void 0 ? {} : { priceFactor },
|
|
110
|
+
...contextOptions === void 0 ? {} : { contextOptions }
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
return models;
|
|
114
|
+
}
|
|
115
|
+
function mergeQoderDiscoveryMetadata(configured, discovered) {
|
|
116
|
+
const catalog = new Map(discovered.map((model) => [model.id, model]));
|
|
117
|
+
return configured.map((model) => {
|
|
118
|
+
const advertised = catalog.get(model.id);
|
|
119
|
+
if (advertised === void 0) return { ...model };
|
|
120
|
+
const merged = { ...model };
|
|
121
|
+
if (advertised.contextWindow !== void 0) merged.contextWindow = Math.min(model.contextWindow ?? advertised.contextWindow, advertised.contextWindow);
|
|
122
|
+
for (const key of discoveredMetadataKeys) delete merged[key];
|
|
123
|
+
for (const key of discoveredMetadataKeys) if (advertised[key] !== void 0) Object.assign(merged, { [key]: advertised[key] });
|
|
124
|
+
return merged;
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
function hasSameQoderDiscoveryMetadata(left, right) {
|
|
128
|
+
return left?.length === right.length && left.every((model, index) => {
|
|
129
|
+
const candidate = right[index];
|
|
130
|
+
return candidate?.id === model.id && candidate.contextWindow === model.contextWindow && discoveredMetadataKeys.every((key) => JSON.stringify(model[key]) === JSON.stringify(candidate[key]));
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
const defaultMaxTokens = 32768;
|
|
134
|
+
const defaultModels = [
|
|
135
|
+
{
|
|
136
|
+
id: "cmodel",
|
|
137
|
+
name: "Cantus (Qoder)",
|
|
138
|
+
description: "Default Global Qoder subscription model for quick validation",
|
|
139
|
+
contextWindow: 1e6,
|
|
140
|
+
maxTokens: defaultMaxTokens,
|
|
141
|
+
supportsImages: true
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
id: "auto",
|
|
145
|
+
name: "Qoder Auto",
|
|
146
|
+
description: "Server-routed Global Qoder model pool",
|
|
147
|
+
contextWindow: 18e4,
|
|
148
|
+
maxTokens: defaultMaxTokens,
|
|
149
|
+
supportsImages: true
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
id: "ultimate",
|
|
153
|
+
name: "Qoder Ultimate",
|
|
154
|
+
description: "Highest-capability Global Qoder model pool",
|
|
155
|
+
contextWindow: 1e6,
|
|
156
|
+
maxTokens: defaultMaxTokens,
|
|
157
|
+
supportsImages: true
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
id: "performance",
|
|
161
|
+
name: "Qoder Performance",
|
|
162
|
+
description: "Performance-oriented Global Qoder model pool",
|
|
163
|
+
contextWindow: 1e6,
|
|
164
|
+
maxTokens: defaultMaxTokens,
|
|
165
|
+
supportsImages: true
|
|
166
|
+
},
|
|
167
|
+
{
|
|
168
|
+
id: "efficient",
|
|
169
|
+
name: "Qoder Efficient",
|
|
170
|
+
description: "Efficiency-oriented Global Qoder model pool",
|
|
171
|
+
contextWindow: 18e4,
|
|
172
|
+
maxTokens: defaultMaxTokens,
|
|
173
|
+
supportsImages: true
|
|
174
|
+
},
|
|
175
|
+
{
|
|
176
|
+
id: "lite",
|
|
177
|
+
name: "Qoder Lite",
|
|
178
|
+
description: "Basic Global Qoder model pool",
|
|
179
|
+
contextWindow: 18e4,
|
|
180
|
+
maxTokens: defaultMaxTokens
|
|
181
|
+
}
|
|
182
|
+
];
|
|
183
|
+
//#endregion
|
|
184
|
+
//#region src/qoder/transport/endpoints.ts
|
|
9
185
|
const qoderRegionEndpoints = {
|
|
10
186
|
global: {
|
|
11
187
|
baseUrl: "https://api3.qoder.sh/",
|
|
12
|
-
openApiUrl: "https://openapi.qoder.sh"
|
|
188
|
+
openApiUrl: "https://openapi.qoder.sh",
|
|
189
|
+
centerUrl: "https://center.qoder.sh"
|
|
13
190
|
},
|
|
14
191
|
china: {
|
|
15
192
|
baseUrl: "https://gateway.qoder.com.cn/",
|
|
16
|
-
openApiUrl: "https://openapi.qoder.com.cn"
|
|
193
|
+
openApiUrl: "https://openapi.qoder.com.cn",
|
|
194
|
+
centerUrl: "https://gateway.qoder.com.cn"
|
|
17
195
|
}
|
|
18
196
|
};
|
|
19
|
-
|
|
20
|
-
const
|
|
197
|
+
/** Signed path of the center image upload route; it never carries an `/algo` prefix. */
|
|
198
|
+
const qoderImageUploadPath = "/api/v2/image/upload";
|
|
199
|
+
qoderRegionEndpoints.global.baseUrl;
|
|
200
|
+
qoderRegionEndpoints.global.openApiUrl;
|
|
21
201
|
function resolveQoderEndpoints(region = "global") {
|
|
22
202
|
return qoderRegionEndpoints[region] ?? qoderRegionEndpoints.global;
|
|
23
203
|
}
|
|
@@ -29,6 +209,11 @@ function getQoderModelListUrl(region = "global") {
|
|
|
29
209
|
const { baseUrl } = resolveQoderEndpoints(region);
|
|
30
210
|
return `${baseUrl}algo/api/v2/model/list?Encode=1`;
|
|
31
211
|
}
|
|
212
|
+
function getQoderImageUploadUrl(region = "global", requestId) {
|
|
213
|
+
const { centerUrl } = resolveQoderEndpoints(region);
|
|
214
|
+
const base = `${centerUrl.replace(/\/+$/u, "")}/algo${qoderImageUploadPath}`;
|
|
215
|
+
return requestId === void 0 ? base : `${base}?request_id=${encodeURIComponent(requestId)}`;
|
|
216
|
+
}
|
|
32
217
|
function getQoderExchangeUrl(region = "global") {
|
|
33
218
|
const { openApiUrl } = resolveQoderEndpoints(region);
|
|
34
219
|
return `${openApiUrl}/api/v1/jobToken/exchange`;
|
|
@@ -41,12 +226,20 @@ function getQoderUsageUrl(region = "global") {
|
|
|
41
226
|
const { openApiUrl } = resolveQoderEndpoints(region);
|
|
42
227
|
return `${openApiUrl}/api/v2/quota/usage`;
|
|
43
228
|
}
|
|
229
|
+
function getQoderUserPlanUrl(region = "global") {
|
|
230
|
+
const { openApiUrl } = resolveQoderEndpoints(region);
|
|
231
|
+
return `${openApiUrl}/api/v2/user/plan`;
|
|
232
|
+
}
|
|
233
|
+
function getQoderUserStatusUrl(region = "global") {
|
|
234
|
+
const { openApiUrl } = resolveQoderEndpoints(region);
|
|
235
|
+
return `${openApiUrl}/api/v3/user/status`;
|
|
236
|
+
}
|
|
44
237
|
//#endregion
|
|
45
|
-
//#region src/errors.ts
|
|
238
|
+
//#region src/qoder/errors.ts
|
|
46
239
|
/**
|
|
47
240
|
* DSH-compatible LLM error representation.
|
|
48
241
|
*
|
|
49
|
-
* @module dsh-provider-qoder/errors
|
|
242
|
+
* @module dsh-provider-qoder/qoder/errors
|
|
50
243
|
*/
|
|
51
244
|
var QoderLlmError = class extends LlmError {
|
|
52
245
|
constructor(message, code = "UNKNOWN_ERROR", options) {
|
|
@@ -57,11 +250,17 @@ function qoderHttpError(message, response) {
|
|
|
57
250
|
const { status } = response;
|
|
58
251
|
const code = status === 401 || status === 403 ? "AUTH" : status === 408 ? "TIMEOUT" : status === 429 ? "RATE_LIMIT" : status >= 500 && status <= 599 ? "SERVER" : status >= 400 && status <= 499 ? "INVALID_REQUEST" : "PROVIDER_ERROR";
|
|
59
252
|
const providerRetryAfterMs = retryAfterMs(response.headers?.get("retry-after") ?? null);
|
|
253
|
+
const requestId = qoderRequestId(response.headers);
|
|
60
254
|
return new QoderLlmError(message, code, {
|
|
61
255
|
status,
|
|
62
|
-
...providerRetryAfterMs === void 0 ? {} : { providerRetryAfterMs }
|
|
256
|
+
...providerRetryAfterMs === void 0 ? {} : { providerRetryAfterMs },
|
|
257
|
+
...requestId === void 0 ? {} : { requestId }
|
|
63
258
|
});
|
|
64
259
|
}
|
|
260
|
+
function qoderRequestId(headers) {
|
|
261
|
+
const normalized = (headers?.get("x-request-id") ?? headers?.get("request-id") ?? headers?.get("x-amzn-requestid"))?.trim();
|
|
262
|
+
return normalized ? ProviderRequestId(normalized) : void 0;
|
|
263
|
+
}
|
|
65
264
|
function retryAfterMs(value, nowMs = Date.now()) {
|
|
66
265
|
const normalized = value?.trim();
|
|
67
266
|
if (!normalized) return void 0;
|
|
@@ -75,7 +274,37 @@ function retryAfterMs(value, nowMs = Date.now()) {
|
|
|
75
274
|
return delayMs > 0 ? delayMs : void 0;
|
|
76
275
|
}
|
|
77
276
|
//#endregion
|
|
78
|
-
//#region src/
|
|
277
|
+
//#region src/qoder/transport/machine-id.ts
|
|
278
|
+
function defaultMachineIdPaths() {
|
|
279
|
+
return [join(homedir(), ".qoder", ".auth", "machine_id"), join(homedir(), ".dsh", "qoder", "machine_id")];
|
|
280
|
+
}
|
|
281
|
+
/** Read Qoder's machine id or create the DSH-owned fallback. */
|
|
282
|
+
function getMachineId(paths = defaultMachineIdPaths()) {
|
|
283
|
+
for (const path of paths) {
|
|
284
|
+
if (!existsSync(path)) continue;
|
|
285
|
+
try {
|
|
286
|
+
const value = readFileSync(path, "utf8").trim();
|
|
287
|
+
if (value) return value;
|
|
288
|
+
} catch {}
|
|
289
|
+
}
|
|
290
|
+
const machineId = crypto.randomUUID();
|
|
291
|
+
const savePath = paths.at(-1);
|
|
292
|
+
if (savePath !== void 0) try {
|
|
293
|
+
mkdirSync(dirname(savePath), { recursive: true });
|
|
294
|
+
writeFileSync(savePath, machineId, {
|
|
295
|
+
encoding: "utf8",
|
|
296
|
+
flag: "wx"
|
|
297
|
+
});
|
|
298
|
+
} catch {
|
|
299
|
+
try {
|
|
300
|
+
const existing = readFileSync(savePath, "utf8").trim();
|
|
301
|
+
if (existing) return existing;
|
|
302
|
+
} catch {}
|
|
303
|
+
}
|
|
304
|
+
return machineId;
|
|
305
|
+
}
|
|
306
|
+
//#endregion
|
|
307
|
+
//#region src/qoder/transport/logging.ts
|
|
79
308
|
const sensitiveKey = /(?:authorization|credential|password|secret|token)/iu;
|
|
80
309
|
const identifierKey = /^(?:accountId|id|uid|userId)$/iu;
|
|
81
310
|
const tokenValue = /\b(?:jrt|jt|pt)-[\w.-]+\b/giu;
|
|
@@ -122,224 +351,22 @@ function redactLogPayload(text) {
|
|
|
122
351
|
return redactLogValue(text);
|
|
123
352
|
}
|
|
124
353
|
}
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
function getMachineId(paths = defaultMachineIdPaths()) {
|
|
132
|
-
for (const path of paths) {
|
|
133
|
-
if (!existsSync(path)) continue;
|
|
134
|
-
try {
|
|
135
|
-
const value = readFileSync(path, "utf8").trim();
|
|
136
|
-
if (value) return value;
|
|
137
|
-
} catch {}
|
|
138
|
-
}
|
|
139
|
-
const machineId = crypto.randomUUID();
|
|
140
|
-
const savePath = paths.at(-1);
|
|
141
|
-
if (savePath !== void 0) try {
|
|
142
|
-
mkdirSync(dirname(savePath), { recursive: true });
|
|
143
|
-
writeFileSync(savePath, machineId, {
|
|
144
|
-
encoding: "utf8",
|
|
145
|
-
flag: "wx"
|
|
146
|
-
});
|
|
147
|
-
} catch {
|
|
148
|
-
try {
|
|
149
|
-
const existing = readFileSync(savePath, "utf8").trim();
|
|
150
|
-
if (existing) return existing;
|
|
151
|
-
} catch {}
|
|
152
|
-
}
|
|
153
|
-
return machineId;
|
|
354
|
+
/** Log one successfully parsed non-stream response through the shared redaction policy. */
|
|
355
|
+
function logParsedResponse(logger, operation, result) {
|
|
356
|
+
logger?.debug?.("[Qoder Response] Parsed", redactLogValue({
|
|
357
|
+
operation,
|
|
358
|
+
result
|
|
359
|
+
}));
|
|
154
360
|
}
|
|
155
361
|
//#endregion
|
|
156
|
-
//#region src/
|
|
157
|
-
const userAgent$1 = "dsh-provider-qoder";
|
|
158
|
-
const expiryBufferMs = 300 * 1e3;
|
|
159
|
-
const defaultExpiryMs = 1440 * 60 * 1e3;
|
|
160
|
-
const defaultAuthTimeoutMs = 15e3;
|
|
161
|
-
function abortedError() {
|
|
162
|
-
return new QoderLlmError("Qoder authentication was aborted.", "ABORTED");
|
|
163
|
-
}
|
|
164
|
-
async function waitForFlight(promise, signal) {
|
|
165
|
-
if (signal === void 0) return promise;
|
|
166
|
-
if (signal.aborted) throw abortedError();
|
|
167
|
-
return new Promise((resolve, reject) => {
|
|
168
|
-
const onAbort = () => reject(abortedError());
|
|
169
|
-
signal.addEventListener("abort", onAbort, { once: true });
|
|
170
|
-
promise.then(resolve, reject).finally(() => signal.removeEventListener("abort", onAbort));
|
|
171
|
-
});
|
|
172
|
-
}
|
|
173
|
-
var QoderAuthService = class {
|
|
174
|
-
cache = /* @__PURE__ */ new Map();
|
|
175
|
-
inFlight = /* @__PURE__ */ new Map();
|
|
176
|
-
fetchImpl;
|
|
177
|
-
timeoutMs;
|
|
178
|
-
resolveMachineId;
|
|
179
|
-
resolveRegion;
|
|
180
|
-
region;
|
|
181
|
-
logger;
|
|
182
|
-
constructor(options = {}) {
|
|
183
|
-
this.fetchImpl = options.fetch ?? globalThis.fetch;
|
|
184
|
-
this.timeoutMs = options.timeoutMs ?? defaultAuthTimeoutMs;
|
|
185
|
-
this.resolveMachineId = options.resolveMachineId ?? getMachineId;
|
|
186
|
-
this.resolveRegion = options.resolveRegion;
|
|
187
|
-
this.region = options.region;
|
|
188
|
-
this.logger = options.logger;
|
|
189
|
-
}
|
|
190
|
-
setRegion(region) {
|
|
191
|
-
this.region = region;
|
|
192
|
-
}
|
|
193
|
-
currentRegion() {
|
|
194
|
-
if (this.resolveRegion) return this.resolveRegion();
|
|
195
|
-
return this.region ?? "global";
|
|
196
|
-
}
|
|
197
|
-
clear(pat) {
|
|
198
|
-
if (pat) {
|
|
199
|
-
for (const key of this.cache.keys()) if (key.endsWith(`:${pat}`) || key === pat) this.cache.delete(key);
|
|
200
|
-
} else this.cache.clear();
|
|
201
|
-
}
|
|
202
|
-
async getCredentials(pat, signal, region) {
|
|
203
|
-
if (!pat || typeof pat !== "string") throw new QoderLlmError("Qoder Personal Access Token is missing or invalid. Configure Qoder in the Qoder settings page.", "MISSING_CREDENTIAL");
|
|
204
|
-
if (signal?.aborted) throw abortedError();
|
|
205
|
-
const effectiveRegion = region ?? this.currentRegion();
|
|
206
|
-
const cacheKey = `${effectiveRegion}:${pat}`;
|
|
207
|
-
const cached = this.cache.get(cacheKey);
|
|
208
|
-
if (cached && cached.expiresAt > Date.now() + expiryBufferMs) return cached.creds;
|
|
209
|
-
let entry = this.inFlight.get(cacheKey);
|
|
210
|
-
if (entry === void 0) {
|
|
211
|
-
const controller = new AbortController();
|
|
212
|
-
const created = {};
|
|
213
|
-
created.controller = controller;
|
|
214
|
-
created.waiters = 0;
|
|
215
|
-
created.settled = false;
|
|
216
|
-
created.timeout = setTimeout(() => controller.abort("authentication timeout"), this.timeoutMs);
|
|
217
|
-
created.promise = this.exchangeAndResolve(pat, controller.signal, effectiveRegion).finally(() => {
|
|
218
|
-
created.settled = true;
|
|
219
|
-
clearTimeout(created.timeout);
|
|
220
|
-
if (this.inFlight.get(cacheKey) === created) this.inFlight.delete(cacheKey);
|
|
221
|
-
});
|
|
222
|
-
entry = created;
|
|
223
|
-
this.inFlight.set(cacheKey, entry);
|
|
224
|
-
}
|
|
225
|
-
entry.waiters++;
|
|
226
|
-
try {
|
|
227
|
-
return await waitForFlight(entry.promise, signal);
|
|
228
|
-
} finally {
|
|
229
|
-
entry.waiters--;
|
|
230
|
-
if (entry.waiters === 0 && !entry.settled) entry.controller.abort("all callers aborted");
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
async exchangeAndResolve(pat, signal, region) {
|
|
234
|
-
let jobToken;
|
|
235
|
-
let expiresAt = Date.now() + defaultExpiryMs;
|
|
236
|
-
try {
|
|
237
|
-
const url = getQoderExchangeUrl(region);
|
|
238
|
-
this.logger?.debug?.("[Qoder Auth] Exchanging PAT for job token", {
|
|
239
|
-
url,
|
|
240
|
-
region
|
|
241
|
-
});
|
|
242
|
-
const response = await this.fetchImpl(url, {
|
|
243
|
-
method: "POST",
|
|
244
|
-
headers: {
|
|
245
|
-
"content-type": "application/json",
|
|
246
|
-
"accept": "application/json",
|
|
247
|
-
"user-agent": userAgent$1,
|
|
248
|
-
"cosy-version": "1.0.1",
|
|
249
|
-
"cosy-clienttype": "5"
|
|
250
|
-
},
|
|
251
|
-
body: JSON.stringify({ personal_token: pat }),
|
|
252
|
-
signal
|
|
253
|
-
});
|
|
254
|
-
this.logger?.debug?.("[Qoder Auth] Exchange completed", {
|
|
255
|
-
status: response.status,
|
|
256
|
-
statusText: response.statusText
|
|
257
|
-
});
|
|
258
|
-
if (!response.ok) {
|
|
259
|
-
const errorText = await response.text();
|
|
260
|
-
this.logger?.error?.("[Qoder Auth] Exchange failed", redactLogPayload(errorText));
|
|
261
|
-
throw qoderHttpError(`Qoder PAT exchange failed with HTTP status ${response.status}.`, response);
|
|
262
|
-
}
|
|
263
|
-
const data = await response.json();
|
|
264
|
-
if (!data.token) throw new QoderLlmError("Qoder PAT exchange returned no job token.", "AUTH");
|
|
265
|
-
jobToken = data.token;
|
|
266
|
-
if (data.expires_at) {
|
|
267
|
-
const parsed = Date.parse(data.expires_at);
|
|
268
|
-
if (!Number.isNaN(parsed)) expiresAt = parsed;
|
|
269
|
-
} else if (typeof data.expires_in === "number" && data.expires_in > 0) expiresAt = Date.now() + data.expires_in;
|
|
270
|
-
} catch (error) {
|
|
271
|
-
if (error instanceof QoderLlmError) throw error;
|
|
272
|
-
if (signal.aborted) throw new QoderLlmError("Qoder authentication request timed out or was cancelled.", "TIMEOUT");
|
|
273
|
-
throw new QoderLlmError("Qoder PAT exchange network request failed.", "TRANSPORT", { cause: error });
|
|
274
|
-
}
|
|
275
|
-
const userInfo = await this.fetchUserInfo(jobToken, signal, region);
|
|
276
|
-
const creds = {
|
|
277
|
-
userID: userInfo.userID,
|
|
278
|
-
authToken: jobToken,
|
|
279
|
-
name: userInfo.name || "Qoder User",
|
|
280
|
-
email: userInfo.email,
|
|
281
|
-
machineID: this.resolveMachineId()
|
|
282
|
-
};
|
|
283
|
-
const cacheKey = `${region}:${pat}`;
|
|
284
|
-
this.cache.set(cacheKey, {
|
|
285
|
-
creds,
|
|
286
|
-
expiresAt
|
|
287
|
-
});
|
|
288
|
-
return creds;
|
|
289
|
-
}
|
|
290
|
-
async fetchUserInfo(jobToken, signal, region) {
|
|
291
|
-
const url = getQoderUserInfoUrl(region);
|
|
292
|
-
this.logger?.debug?.("[Qoder UserInfo] Requesting subscriber profile", { url });
|
|
293
|
-
try {
|
|
294
|
-
const response = await this.fetchImpl(url, {
|
|
295
|
-
headers: {
|
|
296
|
-
"authorization": `Bearer ${jobToken}`,
|
|
297
|
-
"accept": "application/json",
|
|
298
|
-
"user-agent": userAgent$1,
|
|
299
|
-
"cosy-version": "1.0.1",
|
|
300
|
-
"cosy-clienttype": "5"
|
|
301
|
-
},
|
|
302
|
-
signal
|
|
303
|
-
});
|
|
304
|
-
this.logger?.debug?.("[Qoder UserInfo] Request completed", {
|
|
305
|
-
status: response.status,
|
|
306
|
-
statusText: response.statusText
|
|
307
|
-
});
|
|
308
|
-
const text = await response.text();
|
|
309
|
-
if (!response.ok) {
|
|
310
|
-
this.logger?.error?.("[Qoder UserInfo] Request failed", redactLogPayload(text));
|
|
311
|
-
throw qoderHttpError(`Qoder identity lookup failed with HTTP status ${response.status}.`, response);
|
|
312
|
-
}
|
|
313
|
-
let info;
|
|
314
|
-
try {
|
|
315
|
-
info = JSON.parse(text);
|
|
316
|
-
} catch {
|
|
317
|
-
this.logger?.error?.("[Qoder UserInfo] Invalid JSON response", redactLogPayload(text));
|
|
318
|
-
throw new QoderLlmError("Qoder identity lookup returned invalid JSON.", "AUTH");
|
|
319
|
-
}
|
|
320
|
-
this.logger?.debug?.("[Qoder UserInfo] Subscriber profile resolved", redactLogValue(info));
|
|
321
|
-
if (!info.id) throw new QoderLlmError("Qoder identity lookup returned no user id.", "AUTH");
|
|
322
|
-
return {
|
|
323
|
-
userID: info.id,
|
|
324
|
-
email: info.email ?? "",
|
|
325
|
-
name: info.name ?? info.username ?? ""
|
|
326
|
-
};
|
|
327
|
-
} catch (error) {
|
|
328
|
-
if (error instanceof QoderLlmError) throw error;
|
|
329
|
-
if (signal.aborted) throw new QoderLlmError("Qoder identity lookup timed out or was cancelled.", "TIMEOUT");
|
|
330
|
-
throw new QoderLlmError("Qoder identity lookup network request failed.", "TRANSPORT", { cause: error });
|
|
331
|
-
}
|
|
332
|
-
}
|
|
333
|
-
};
|
|
334
|
-
//#endregion
|
|
335
|
-
//#region src/cosy.ts
|
|
362
|
+
//#region src/qoder/transport/wire/cosy.ts
|
|
336
363
|
/**
|
|
337
364
|
* COSY authentication headers and signature algorithm for Qoder services.
|
|
338
365
|
*
|
|
339
366
|
* Implements RSA + AES + MD5 signature generation required by the upstream
|
|
340
367
|
* Qoder gateway.
|
|
341
368
|
*
|
|
342
|
-
* @module dsh-provider-qoder/cosy
|
|
369
|
+
* @module dsh-provider-qoder/qoder/transport/wire/cosy
|
|
343
370
|
*/
|
|
344
371
|
const qoderRSAPublicKey = `-----BEGIN PUBLIC KEY-----
|
|
345
372
|
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDA8iMH5c02LilrsERw9t6Pv5Nc
|
|
@@ -347,8 +374,8 @@ MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDA8iMH5c02LilrsERw9t6Pv5Nc
|
|
|
347
374
|
6HRkPJ7S236FZz73In/KVuLnwI8JJ2CbuJap8kvheCCZpmAWpb/cPx/3Vr/J6I17
|
|
348
375
|
XcW+ML9FoCI6AOvOzwIDAQAB
|
|
349
376
|
-----END PUBLIC KEY-----`;
|
|
350
|
-
const qoderIdeVersion = "1.1.
|
|
351
|
-
const
|
|
377
|
+
const qoderIdeVersion = "1.1.47";
|
|
378
|
+
const defaultUserAgent = `qoder/${qoderIdeVersion}`;
|
|
352
379
|
const qoderDataPolicy = "disagree";
|
|
353
380
|
const qoderLoginVersion = "v2";
|
|
354
381
|
const qoderMachineOs = process.platform === "win32" ? process.arch === "arm64" ? "aarch64_windows" : "x86_64_windows" : process.arch === "arm64" ? "aarch64_linux" : "x86_64_linux";
|
|
@@ -409,7 +436,7 @@ function buildAuthHeaders(body, requestURL, creds) {
|
|
|
409
436
|
"Cosy-Machinetoken": machineID,
|
|
410
437
|
"Cosy-Machinetype": qoderMachineTypeMagic,
|
|
411
438
|
"Cosy-Machineos": qoderMachineOs,
|
|
412
|
-
"Cosy-Clienttype":
|
|
439
|
+
"Cosy-Clienttype": "5",
|
|
413
440
|
"Cosy-Clientip": "127.0.0.1",
|
|
414
441
|
"Cosy-Bodyhash": bodyHash,
|
|
415
442
|
"Cosy-Bodylength": bodyLen,
|
|
@@ -421,48 +448,421 @@ function buildAuthHeaders(body, requestURL, creds) {
|
|
|
421
448
|
"X-Request-Id": crypto.randomUUID()
|
|
422
449
|
};
|
|
423
450
|
}
|
|
451
|
+
const defaultMaxJsonBytes = 2 * 1024 * 1024;
|
|
452
|
+
const defaultMaxErrorBytes = 16 * 1024;
|
|
453
|
+
const metadataRetryBaseDelayMs = 200;
|
|
454
|
+
const maxProviderRetryDelayMs = 1e4;
|
|
455
|
+
function opaqueCredentialKey(value) {
|
|
456
|
+
return createHash("sha256").update(value).digest("hex");
|
|
457
|
+
}
|
|
458
|
+
function retryableMetadataError(error) {
|
|
459
|
+
return error instanceof QoderLlmError && [
|
|
460
|
+
"RATE_LIMIT",
|
|
461
|
+
"SERVER",
|
|
462
|
+
"TIMEOUT",
|
|
463
|
+
"TRANSPORT"
|
|
464
|
+
].includes(error.code);
|
|
465
|
+
}
|
|
466
|
+
function abortableDelay(ms, signal) {
|
|
467
|
+
if (signal.aborted) return Promise.reject(new DOMException("aborted", "AbortError"));
|
|
468
|
+
return new Promise((resolve, reject) => {
|
|
469
|
+
const timer = setTimeout(() => {
|
|
470
|
+
signal.removeEventListener("abort", onAbort);
|
|
471
|
+
resolve();
|
|
472
|
+
}, ms);
|
|
473
|
+
const onAbort = () => {
|
|
474
|
+
clearTimeout(timer);
|
|
475
|
+
reject(new DOMException("aborted", "AbortError"));
|
|
476
|
+
};
|
|
477
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
478
|
+
});
|
|
479
|
+
}
|
|
480
|
+
async function retryMetadataRead(signal, operation) {
|
|
481
|
+
try {
|
|
482
|
+
return await operation();
|
|
483
|
+
} catch (error) {
|
|
484
|
+
if (!retryableMetadataError(error) || signal.aborted) throw error;
|
|
485
|
+
const providerDelay = error.failure.providerRetryAfterMs;
|
|
486
|
+
await abortableDelay(providerDelay === void 0 ? metadataRetryBaseDelayMs + Math.floor(Math.random() * 41) : Math.min(providerDelay, maxProviderRetryDelayMs), signal);
|
|
487
|
+
return operation();
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
function withDeadline(signal, timeoutMs) {
|
|
491
|
+
const timeoutSignal = AbortSignal.timeout(timeoutMs);
|
|
492
|
+
return {
|
|
493
|
+
timeoutSignal,
|
|
494
|
+
signal: signal === void 0 ? timeoutSignal : AbortSignal.any([signal, timeoutSignal])
|
|
495
|
+
};
|
|
496
|
+
}
|
|
497
|
+
async function readLimitedText(response, maxBytes, label) {
|
|
498
|
+
const declaredLength = Number(response.headers.get("content-length"));
|
|
499
|
+
if (Number.isFinite(declaredLength) && declaredLength > maxBytes) {
|
|
500
|
+
await response.body?.cancel().catch(() => void 0);
|
|
501
|
+
throw new QoderLlmError(`${label} exceeded its response size limit.`, "MALFORMED_RESPONSE");
|
|
502
|
+
}
|
|
503
|
+
if (!response.body) return "";
|
|
504
|
+
const reader = response.body.getReader();
|
|
505
|
+
const decoder = new TextDecoder();
|
|
506
|
+
let bytes = 0;
|
|
507
|
+
let text = "";
|
|
508
|
+
try {
|
|
509
|
+
while (true) {
|
|
510
|
+
const { done, value } = await reader.read();
|
|
511
|
+
if (done) break;
|
|
512
|
+
bytes += value.byteLength;
|
|
513
|
+
if (bytes > maxBytes) throw new QoderLlmError(`${label} exceeded its response size limit.`, "MALFORMED_RESPONSE");
|
|
514
|
+
text += decoder.decode(value, { stream: true });
|
|
515
|
+
}
|
|
516
|
+
return text + decoder.decode();
|
|
517
|
+
} finally {
|
|
518
|
+
await reader.cancel().catch(() => void 0);
|
|
519
|
+
reader.releaseLock();
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
var SingleFlight = class {
|
|
523
|
+
flights = /* @__PURE__ */ new Map();
|
|
524
|
+
run(key, signal, start, abortedError) {
|
|
525
|
+
if (signal?.aborted) return Promise.reject(abortedError());
|
|
526
|
+
let flight = this.flights.get(key);
|
|
527
|
+
if (flight === void 0 || flight.controller.signal.aborted) {
|
|
528
|
+
const controller = new AbortController();
|
|
529
|
+
const created = {};
|
|
530
|
+
created.controller = controller;
|
|
531
|
+
created.settled = false;
|
|
532
|
+
created.waiters = 0;
|
|
533
|
+
created.promise = start(controller.signal).finally(() => {
|
|
534
|
+
created.settled = true;
|
|
535
|
+
if (this.flights.get(key) === created) this.flights.delete(key);
|
|
536
|
+
});
|
|
537
|
+
flight = created;
|
|
538
|
+
this.flights.set(key, created);
|
|
539
|
+
}
|
|
540
|
+
flight.waiters++;
|
|
541
|
+
return new Promise((resolve, reject) => {
|
|
542
|
+
let finished = false;
|
|
543
|
+
const finish = (callback) => {
|
|
544
|
+
if (finished) return;
|
|
545
|
+
finished = true;
|
|
546
|
+
signal?.removeEventListener("abort", onAbort);
|
|
547
|
+
flight.waiters--;
|
|
548
|
+
if (flight.waiters === 0 && !flight.settled) {
|
|
549
|
+
if (this.flights.get(key) === flight) this.flights.delete(key);
|
|
550
|
+
flight.controller.abort("all callers aborted");
|
|
551
|
+
}
|
|
552
|
+
callback();
|
|
553
|
+
};
|
|
554
|
+
const onAbort = () => finish(() => reject(abortedError()));
|
|
555
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
556
|
+
flight.promise.then((value) => finish(() => resolve(value)), (error) => finish(() => reject(error)));
|
|
557
|
+
});
|
|
558
|
+
}
|
|
559
|
+
};
|
|
560
|
+
async function openApiJsonRequest(fetchImpl, options) {
|
|
561
|
+
const method = options.method ?? (options.body !== void 0 ? "POST" : "GET");
|
|
562
|
+
const timeoutMs = options.timeoutMs ?? 15e3;
|
|
563
|
+
const timeoutSignal = AbortSignal.timeout(timeoutMs);
|
|
564
|
+
const requestSignal = options.signal === void 0 ? timeoutSignal : AbortSignal.any([options.signal, timeoutSignal]);
|
|
565
|
+
const startedAt = performance.now();
|
|
566
|
+
options.logger?.debug?.(`[Qoder ${options.operation}] Requesting`, {
|
|
567
|
+
url: options.url,
|
|
568
|
+
method
|
|
569
|
+
});
|
|
570
|
+
try {
|
|
571
|
+
const headers = {
|
|
572
|
+
accept: "application/json",
|
|
573
|
+
"user-agent": options.userAgent ?? defaultUserAgent,
|
|
574
|
+
"cosy-version": "1.0.1",
|
|
575
|
+
"cosy-clienttype": "5",
|
|
576
|
+
...options.headers
|
|
577
|
+
};
|
|
578
|
+
if (options.token) headers.authorization = `Bearer ${options.token}`;
|
|
579
|
+
if (options.machineId) {
|
|
580
|
+
headers["Cosy-MachineToken"] = options.machineId;
|
|
581
|
+
headers["Cosy-MachineType"] = "host";
|
|
582
|
+
}
|
|
583
|
+
let bodyText;
|
|
584
|
+
if (options.body !== void 0) {
|
|
585
|
+
headers["content-type"] = "application/json";
|
|
586
|
+
bodyText = typeof options.body === "string" ? options.body : JSON.stringify(options.body);
|
|
587
|
+
}
|
|
588
|
+
const response = await fetchImpl(options.url, {
|
|
589
|
+
method,
|
|
590
|
+
headers,
|
|
591
|
+
body: bodyText,
|
|
592
|
+
signal: requestSignal
|
|
593
|
+
});
|
|
594
|
+
options.logger?.debug?.(`[Qoder ${options.operation}] Request completed`, {
|
|
595
|
+
status: response.status,
|
|
596
|
+
statusText: response.statusText,
|
|
597
|
+
durationMs: Math.round(performance.now() - startedAt),
|
|
598
|
+
...qoderRequestId(response.headers) === void 0 ? {} : { requestId: qoderRequestId(response.headers) }
|
|
599
|
+
});
|
|
600
|
+
const text = await readLimitedText(response, response.ok ? defaultMaxJsonBytes : defaultMaxErrorBytes, `Qoder ${options.operation} response`);
|
|
601
|
+
if (!response.ok) {
|
|
602
|
+
options.logger?.error?.(`[Qoder ${options.operation}] Request failed`, redactLogPayload(text));
|
|
603
|
+
throw qoderHttpError(`Failed to execute Qoder ${options.operation} with status ${response.status}.`, response);
|
|
604
|
+
}
|
|
605
|
+
let data;
|
|
606
|
+
try {
|
|
607
|
+
data = JSON.parse(text);
|
|
608
|
+
} catch {
|
|
609
|
+
options.logger?.error?.(`[Qoder ${options.operation}] Invalid JSON response`, redactLogPayload(text));
|
|
610
|
+
throw new QoderLlmError(`Failed to parse Qoder ${options.operation} JSON response`, "MALFORMED_RESPONSE");
|
|
611
|
+
}
|
|
612
|
+
if (options.logCategory) logParsedResponse(options.logger, options.logCategory, data);
|
|
613
|
+
return data;
|
|
614
|
+
} catch (error) {
|
|
615
|
+
if (error instanceof QoderLlmError) throw error;
|
|
616
|
+
if (options.signal?.aborted) throw new QoderLlmError(`Qoder ${options.operation} request was aborted.`, "ABORTED");
|
|
617
|
+
if (timeoutSignal.aborted) throw new QoderLlmError(`Qoder ${options.operation} request timed out.`, "TIMEOUT");
|
|
618
|
+
throw new QoderLlmError(`Qoder ${options.operation} network request failed.`, "TRANSPORT", { cause: error });
|
|
619
|
+
}
|
|
620
|
+
}
|
|
424
621
|
//#endregion
|
|
425
|
-
//#region src/
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
622
|
+
//#region src/qoder/transport/auth.ts
|
|
623
|
+
const expiryBufferMs = 300 * 1e3;
|
|
624
|
+
const defaultExpiryMs = 1440 * 60 * 1e3;
|
|
625
|
+
const defaultAuthTimeoutMs = 15e3;
|
|
626
|
+
function abortedError() {
|
|
627
|
+
return new QoderLlmError("Qoder authentication was aborted.", "ABORTED");
|
|
628
|
+
}
|
|
629
|
+
async function waitForFlight(promise, signal) {
|
|
630
|
+
if (signal === void 0) return promise;
|
|
631
|
+
if (signal.aborted) throw abortedError();
|
|
632
|
+
return new Promise((resolve, reject) => {
|
|
633
|
+
const onAbort = () => reject(abortedError());
|
|
634
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
635
|
+
promise.then(resolve, reject).finally(() => signal.removeEventListener("abort", onAbort));
|
|
636
|
+
});
|
|
637
|
+
}
|
|
638
|
+
var QoderAuthService = class {
|
|
639
|
+
cache = /* @__PURE__ */ new Map();
|
|
640
|
+
inFlight = /* @__PURE__ */ new Map();
|
|
641
|
+
fetchImpl;
|
|
642
|
+
timeoutMs;
|
|
643
|
+
resolveMachineId;
|
|
644
|
+
region;
|
|
645
|
+
logger;
|
|
646
|
+
constructor(options = {}) {
|
|
647
|
+
this.fetchImpl = options.fetch ?? globalThis.fetch;
|
|
648
|
+
this.timeoutMs = options.timeoutMs ?? defaultAuthTimeoutMs;
|
|
649
|
+
this.resolveMachineId = options.resolveMachineId ?? getMachineId;
|
|
650
|
+
this.region = options.region ?? "global";
|
|
651
|
+
this.logger = options.logger;
|
|
652
|
+
}
|
|
653
|
+
clear(pat) {
|
|
654
|
+
if (pat) this.cache.delete(`${this.region}:${opaqueCredentialKey(pat)}`);
|
|
655
|
+
else this.cache.clear();
|
|
656
|
+
}
|
|
657
|
+
async getCredentials(pat, signal) {
|
|
658
|
+
if (!pat || typeof pat !== "string") throw new QoderLlmError("Qoder Personal Access Token is missing or invalid. Configure Qoder in the Qoder settings page.", "MISSING_CREDENTIAL");
|
|
659
|
+
if (signal?.aborted) throw abortedError();
|
|
660
|
+
const cacheKey = `${this.region}:${opaqueCredentialKey(pat)}`;
|
|
661
|
+
const cached = this.cache.get(cacheKey);
|
|
662
|
+
if (cached && cached.expiresAt > Date.now() + expiryBufferMs) return cached.creds;
|
|
663
|
+
let entry = this.inFlight.get(cacheKey);
|
|
664
|
+
if (entry === void 0 || entry.controller.signal.aborted) {
|
|
665
|
+
const controller = new AbortController();
|
|
666
|
+
const created = {};
|
|
667
|
+
created.controller = controller;
|
|
668
|
+
created.waiters = 0;
|
|
669
|
+
created.settled = false;
|
|
670
|
+
created.timeout = setTimeout(() => controller.abort("authentication timeout"), this.timeoutMs);
|
|
671
|
+
created.promise = this.exchangeAndResolve(pat, controller.signal).finally(() => {
|
|
672
|
+
created.settled = true;
|
|
673
|
+
clearTimeout(created.timeout);
|
|
674
|
+
if (this.inFlight.get(cacheKey) === created) this.inFlight.delete(cacheKey);
|
|
675
|
+
});
|
|
676
|
+
entry = created;
|
|
677
|
+
this.inFlight.set(cacheKey, entry);
|
|
678
|
+
}
|
|
679
|
+
entry.waiters++;
|
|
680
|
+
try {
|
|
681
|
+
return await waitForFlight(entry.promise, signal);
|
|
682
|
+
} finally {
|
|
683
|
+
entry.waiters--;
|
|
684
|
+
if (entry.waiters === 0 && !entry.settled) {
|
|
685
|
+
if (this.inFlight.get(cacheKey) === entry) this.inFlight.delete(cacheKey);
|
|
686
|
+
entry.controller.abort("all callers aborted");
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
async exchangeAndResolve(pat, signal) {
|
|
691
|
+
let jobToken;
|
|
692
|
+
let expiresAt = Date.now() + defaultExpiryMs;
|
|
693
|
+
const data = await openApiJsonRequest(this.fetchImpl, {
|
|
694
|
+
url: getQoderExchangeUrl(this.region),
|
|
695
|
+
body: { personal_token: pat },
|
|
696
|
+
signal,
|
|
697
|
+
timeoutMs: this.timeoutMs,
|
|
698
|
+
logger: this.logger,
|
|
699
|
+
operation: "Auth",
|
|
700
|
+
logCategory: "auth.exchange"
|
|
701
|
+
});
|
|
702
|
+
if (!data.token) throw new QoderLlmError("Qoder PAT exchange returned no job token.", "AUTH");
|
|
703
|
+
jobToken = data.token;
|
|
704
|
+
if (data.expires_at) {
|
|
705
|
+
const parsed = Date.parse(data.expires_at);
|
|
706
|
+
if (!Number.isNaN(parsed)) expiresAt = parsed;
|
|
707
|
+
} else if (typeof data.expires_in === "number" && data.expires_in > 0) expiresAt = Date.now() + data.expires_in;
|
|
708
|
+
const userInfo = await retryMetadataRead(signal, () => this.fetchUserInfo(jobToken, signal));
|
|
709
|
+
const creds = {
|
|
710
|
+
userID: userInfo.userID,
|
|
711
|
+
authToken: jobToken,
|
|
712
|
+
name: userInfo.name || "Qoder User",
|
|
713
|
+
email: userInfo.email,
|
|
714
|
+
machineID: this.resolveMachineId()
|
|
715
|
+
};
|
|
716
|
+
const cacheKey = `${this.region}:${opaqueCredentialKey(pat)}`;
|
|
717
|
+
this.cache.set(cacheKey, {
|
|
718
|
+
creds,
|
|
719
|
+
expiresAt
|
|
720
|
+
});
|
|
721
|
+
return creds;
|
|
722
|
+
}
|
|
723
|
+
async fetchUserInfo(jobToken, signal) {
|
|
724
|
+
const info = await openApiJsonRequest(this.fetchImpl, {
|
|
725
|
+
url: getQoderUserInfoUrl(this.region),
|
|
726
|
+
token: jobToken,
|
|
727
|
+
signal,
|
|
728
|
+
timeoutMs: this.timeoutMs,
|
|
729
|
+
logger: this.logger,
|
|
730
|
+
operation: "UserInfo",
|
|
731
|
+
logCategory: "auth.user-info"
|
|
732
|
+
});
|
|
733
|
+
if (!info.id) throw new QoderLlmError("Qoder identity lookup returned no user id.", "AUTH");
|
|
734
|
+
return {
|
|
735
|
+
userID: info.id,
|
|
736
|
+
email: info.email ?? "",
|
|
737
|
+
name: info.name ?? info.username ?? ""
|
|
738
|
+
};
|
|
739
|
+
}
|
|
740
|
+
};
|
|
741
|
+
//#endregion
|
|
742
|
+
//#region src/qoder/transport/catalog-reader.ts
|
|
743
|
+
async function fetchQoderModels(credentials, options = {}) {
|
|
744
|
+
const url = getQoderModelListUrl(options.region);
|
|
745
|
+
const fetchImpl = options.fetch ?? globalThis.fetch;
|
|
746
|
+
const startedAt = performance.now();
|
|
747
|
+
const deadline = withDeadline(options.signal, options.timeoutMs ?? 15e3);
|
|
748
|
+
options.logger?.debug?.("[Qoder Models] Requesting model catalog", { url });
|
|
749
|
+
try {
|
|
750
|
+
const response = await fetchImpl(url, {
|
|
751
|
+
method: "GET",
|
|
752
|
+
headers: {
|
|
753
|
+
accept: "application/json",
|
|
754
|
+
...buildAuthHeaders(null, url, credentials)
|
|
755
|
+
},
|
|
756
|
+
signal: deadline.signal
|
|
757
|
+
});
|
|
758
|
+
options.logger?.debug?.("[Qoder Models] Catalog request completed", {
|
|
759
|
+
url,
|
|
760
|
+
status: response.status,
|
|
761
|
+
durationMs: Math.round(performance.now() - startedAt),
|
|
762
|
+
...qoderRequestId(response.headers) === void 0 ? {} : { requestId: qoderRequestId(response.headers) }
|
|
763
|
+
});
|
|
764
|
+
const text = await readLimitedText(response, response.ok ? defaultMaxJsonBytes : defaultMaxErrorBytes, "Qoder model discovery response");
|
|
765
|
+
if (!response.ok) {
|
|
766
|
+
options.logger?.error?.("[Qoder Models] Catalog request failed", redactLogPayload(text));
|
|
767
|
+
throw qoderHttpError(`Qoder model discovery failed with HTTP status ${response.status}.`, response);
|
|
768
|
+
}
|
|
769
|
+
let payload;
|
|
770
|
+
try {
|
|
771
|
+
payload = JSON.parse(text);
|
|
772
|
+
} catch {
|
|
773
|
+
throw new QoderLlmError("Qoder model discovery returned invalid JSON.", "MALFORMED_RESPONSE");
|
|
449
774
|
}
|
|
775
|
+
const models = normalizeQoderModels(payload, (conflict) => {
|
|
776
|
+
options.logger?.warn?.("[Qoder Models] Conflicting catalog defaults; using fallback", { conflict });
|
|
777
|
+
});
|
|
778
|
+
if (models.length === 0) throw new QoderLlmError("Qoder model discovery returned no enabled models.", "EMPTY_RESPONSE");
|
|
779
|
+
return models;
|
|
780
|
+
} catch (error) {
|
|
781
|
+
if (error instanceof QoderLlmError) throw error;
|
|
782
|
+
if (options.signal?.aborted) throw new QoderLlmError("Qoder model discovery was aborted.", "ABORTED");
|
|
783
|
+
if (deadline.timeoutSignal.aborted) throw new QoderLlmError("Qoder model discovery timed out.", "TIMEOUT");
|
|
784
|
+
options.logger?.error?.("[Qoder Models] Catalog network request failed", redactLogValue(error));
|
|
785
|
+
throw new QoderLlmError("Qoder model discovery network request failed.", "TRANSPORT", { cause: error });
|
|
450
786
|
}
|
|
451
|
-
return out;
|
|
452
787
|
}
|
|
453
788
|
//#endregion
|
|
454
|
-
//#region src/translate.ts
|
|
789
|
+
//#region src/qoder/transport/wire/translate.ts
|
|
455
790
|
function unsupported(message) {
|
|
456
791
|
return new QoderLlmError(message, "UNSUPPORTED_CONTENT");
|
|
457
792
|
}
|
|
458
793
|
function toolResultText(block) {
|
|
459
794
|
let text = "";
|
|
460
795
|
for (const nested of block.content) {
|
|
796
|
+
if (nested.type === "image") continue;
|
|
461
797
|
if (nested.type !== "text") throw unsupported(`Qoder tool results support text only; received nested ${String(nested.type)} content.`);
|
|
462
798
|
text += nested.text;
|
|
463
799
|
}
|
|
464
800
|
return text;
|
|
465
801
|
}
|
|
802
|
+
/**
|
|
803
|
+
* Check message shapes without performing any provider I/O.
|
|
804
|
+
*
|
|
805
|
+
* Callers run this before resolving credentials so an invalid request never
|
|
806
|
+
* consumes a Qoder subscription.
|
|
807
|
+
*/
|
|
808
|
+
function validateMessageShapes(messages) {
|
|
809
|
+
for (const message of messages) {
|
|
810
|
+
const toolResults = message.content.filter((block) => block.type === "tool-result");
|
|
811
|
+
if (toolResults.length > 0) {
|
|
812
|
+
if (message.role !== "user" || toolResults.length !== message.content.length) throw unsupported("Qoder tool-result messages cannot contain sibling content or use a non-user role.");
|
|
813
|
+
for (const result of toolResults) toolResultText(result);
|
|
814
|
+
continue;
|
|
815
|
+
}
|
|
816
|
+
for (const block of message.content) {
|
|
817
|
+
if (block.type === "text") continue;
|
|
818
|
+
if (block.type === "image") {
|
|
819
|
+
if (message.role !== "user") throw unsupported("Qoder image content is valid only in user messages.");
|
|
820
|
+
continue;
|
|
821
|
+
}
|
|
822
|
+
if (block.type === "tool-call") {
|
|
823
|
+
if (message.role !== "assistant") throw unsupported("Qoder tool calls are valid only in assistant messages.");
|
|
824
|
+
continue;
|
|
825
|
+
}
|
|
826
|
+
if (block.type === "reasoning") {
|
|
827
|
+
if (message.role !== "assistant") throw unsupported("Qoder historical reasoning is valid only in assistant messages.");
|
|
828
|
+
continue;
|
|
829
|
+
}
|
|
830
|
+
throw unsupported(`Qoder transport encountered unsupported block type: ${String(block.type)}`);
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
/** Reject a batch that exceeds the deployment image policy before any upload work starts. */
|
|
835
|
+
function enforceImageLimits(images, attachments) {
|
|
836
|
+
const limits = attachments.imageLimits;
|
|
837
|
+
if (images.length > limits.maxImagesPerMessage) throw unsupported(`Qoder accepts at most ${limits.maxImagesPerMessage} images per message; received ${images.length}.`);
|
|
838
|
+
let total = 0;
|
|
839
|
+
for (const image of images) total += image.attachment.bytes;
|
|
840
|
+
if (total > limits.maxMessageImageBytes) throw unsupported("Qoder message image content exceeds the configured total byte limit.");
|
|
841
|
+
}
|
|
842
|
+
async function resolveImagePart(block, context) {
|
|
843
|
+
const { attachments, uploader, credentials, signal } = context;
|
|
844
|
+
if (attachments === void 0) throw new QoderLlmError("Qoder image input requires the DSH attachment service.", "ATTACHMENT");
|
|
845
|
+
let image;
|
|
846
|
+
try {
|
|
847
|
+
const limits = attachments.imageLimits;
|
|
848
|
+
image = await attachments.readImageRequest(block.attachment, {
|
|
849
|
+
maxPixels: limits.maxImagePixels,
|
|
850
|
+
maxBytes: limits.maxImageBytes
|
|
851
|
+
}, signal);
|
|
852
|
+
} catch (error) {
|
|
853
|
+
if (signal?.aborted) throw new QoderLlmError("Qoder image preparation was aborted.", "ABORTED", { cause: error });
|
|
854
|
+
if (error instanceof QoderLlmError) throw error;
|
|
855
|
+
throw new QoderLlmError("Qoder could not prepare an image attachment.", "ATTACHMENT", { cause: error });
|
|
856
|
+
}
|
|
857
|
+
if (uploader !== void 0 && credentials !== void 0) return {
|
|
858
|
+
type: "image_url",
|
|
859
|
+
image_url: { url: await uploader.resolveImageUrl(image, credentials, signal) }
|
|
860
|
+
};
|
|
861
|
+
return {
|
|
862
|
+
type: "image_url",
|
|
863
|
+
image_url: { url: `data:${image.mediaType};base64,${Buffer.from(image.data).toString("base64")}` }
|
|
864
|
+
};
|
|
865
|
+
}
|
|
466
866
|
function translateTools(tools) {
|
|
467
867
|
return (tools ?? []).map((tool) => ({
|
|
468
868
|
type: "function",
|
|
@@ -473,7 +873,14 @@ function translateTools(tools) {
|
|
|
473
873
|
}
|
|
474
874
|
}));
|
|
475
875
|
}
|
|
476
|
-
function validateAndTranslateMessages(messages, systemPrompt) {
|
|
876
|
+
async function validateAndTranslateMessages(messages, systemPrompt, attachments, signal, pipeline) {
|
|
877
|
+
validateMessageShapes(messages);
|
|
878
|
+
const context = {
|
|
879
|
+
attachments,
|
|
880
|
+
signal,
|
|
881
|
+
uploader: pipeline?.uploader,
|
|
882
|
+
credentials: pipeline?.credentials
|
|
883
|
+
};
|
|
477
884
|
const output = [];
|
|
478
885
|
if (typeof systemPrompt === "string" && systemPrompt.trim().length > 0) output.push({
|
|
479
886
|
role: "system",
|
|
@@ -483,23 +890,50 @@ function validateAndTranslateMessages(messages, systemPrompt) {
|
|
|
483
890
|
const toolResults = message.content.filter((block) => block.type === "tool-result");
|
|
484
891
|
if (toolResults.length > 0) {
|
|
485
892
|
if (message.role !== "user" || toolResults.length !== message.content.length) throw unsupported("Qoder tool-result messages cannot contain sibling content or use a non-user role.");
|
|
486
|
-
for (const result of toolResults)
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
893
|
+
for (const result of toolResults) {
|
|
894
|
+
output.push({
|
|
895
|
+
role: "tool",
|
|
896
|
+
tool_call_id: String(result.toolCallId),
|
|
897
|
+
content: toolResultText(result)
|
|
898
|
+
});
|
|
899
|
+
const images = result.content.filter((block) => block.type === "image");
|
|
900
|
+
if (images.length > 0) {
|
|
901
|
+
if (attachments !== void 0) enforceImageLimits(images, attachments);
|
|
902
|
+
output.push({
|
|
903
|
+
role: "user",
|
|
904
|
+
content: [{
|
|
905
|
+
type: "text",
|
|
906
|
+
text: `[${images.length} image${images.length === 1 ? "" : "s"} returned by the previous tool call]`
|
|
907
|
+
}, ...await Promise.all(images.map((image) => resolveImagePart(image, context)))]
|
|
908
|
+
});
|
|
909
|
+
}
|
|
910
|
+
}
|
|
491
911
|
continue;
|
|
492
912
|
}
|
|
493
913
|
let text = "";
|
|
914
|
+
const userContent = [];
|
|
915
|
+
const pendingImages = [];
|
|
916
|
+
let hasImage = false;
|
|
494
917
|
const toolCalls = [];
|
|
495
918
|
for (const block of message.content) {
|
|
496
919
|
if (block.type === "text") {
|
|
497
920
|
text += block.text;
|
|
921
|
+
if (message.role === "user") userContent.push({
|
|
922
|
+
type: "text",
|
|
923
|
+
text: block.text
|
|
924
|
+
});
|
|
925
|
+
continue;
|
|
926
|
+
}
|
|
927
|
+
if (block.type === "image") {
|
|
928
|
+
hasImage = true;
|
|
929
|
+
pendingImages.push({
|
|
930
|
+
slot: userContent.length,
|
|
931
|
+
block
|
|
932
|
+
});
|
|
933
|
+
userContent.push(void 0);
|
|
498
934
|
continue;
|
|
499
935
|
}
|
|
500
|
-
if (block.type === "image") throw unsupported("Qoder transport does not support image content.");
|
|
501
936
|
if (block.type === "tool-call") {
|
|
502
|
-
if (message.role !== "assistant") throw unsupported("Qoder tool calls are valid only in assistant messages.");
|
|
503
937
|
toolCalls.push({
|
|
504
938
|
id: String(block.id),
|
|
505
939
|
type: "function",
|
|
@@ -510,12 +944,7 @@ function validateAndTranslateMessages(messages, systemPrompt) {
|
|
|
510
944
|
});
|
|
511
945
|
continue;
|
|
512
946
|
}
|
|
513
|
-
if (block.type === "
|
|
514
|
-
if (block.type === "reasoning") {
|
|
515
|
-
if (message.role !== "assistant") throw unsupported("Qoder historical reasoning is valid only in assistant messages.");
|
|
516
|
-
continue;
|
|
517
|
-
}
|
|
518
|
-
throw unsupported(`Qoder transport encountered unsupported block type: ${String(block.type)}`);
|
|
947
|
+
if (block.type === "reasoning") continue;
|
|
519
948
|
}
|
|
520
949
|
if (message.role === "assistant") {
|
|
521
950
|
if (!text && toolCalls.length === 0) continue;
|
|
@@ -526,15 +955,21 @@ function validateAndTranslateMessages(messages, systemPrompt) {
|
|
|
526
955
|
});
|
|
527
956
|
continue;
|
|
528
957
|
}
|
|
958
|
+
if (pendingImages.length > 0) {
|
|
959
|
+
if (attachments !== void 0) enforceImageLimits(pendingImages.map((pending) => pending.block), attachments);
|
|
960
|
+
await Promise.all(pendingImages.map(async (pending) => {
|
|
961
|
+
userContent[pending.slot] = await resolveImagePart(pending.block, context);
|
|
962
|
+
}));
|
|
963
|
+
}
|
|
529
964
|
output.push({
|
|
530
965
|
role: message.role,
|
|
531
|
-
content: text
|
|
966
|
+
content: hasImage ? userContent.filter((part) => part !== void 0) : text
|
|
532
967
|
});
|
|
533
968
|
}
|
|
534
969
|
return output;
|
|
535
970
|
}
|
|
536
971
|
//#endregion
|
|
537
|
-
//#region src/serialize.ts
|
|
972
|
+
//#region src/qoder/transport/wire/serialize.ts
|
|
538
973
|
/** Build the minimum qodercli request envelope for a validated DSH request. */
|
|
539
974
|
function stableHash(prefix, ...inputs) {
|
|
540
975
|
const hash = crypto.createHash("sha256");
|
|
@@ -558,28 +993,46 @@ function stableChatRecordId(model, messages, tools, maxTokens) {
|
|
|
558
993
|
hash.update(`mt=${maxTokens}`);
|
|
559
994
|
return hash.digest("hex").slice(0, 16);
|
|
560
995
|
}
|
|
561
|
-
|
|
996
|
+
/**
|
|
997
|
+
* Reject an unusable request before any credential resolution or provider I/O.
|
|
998
|
+
*
|
|
999
|
+
* Image publication needs credentials, so message translation now runs after
|
|
1000
|
+
* authentication. This static pass preserves the guarantee that a request the
|
|
1001
|
+
* provider cannot serve never reaches the network.
|
|
1002
|
+
*/
|
|
1003
|
+
function validateQoderRequestShape(options, model) {
|
|
562
1004
|
if (options.reasoningEffort !== void 0) {
|
|
563
1005
|
const effort = String(options.reasoningEffort);
|
|
564
1006
|
if (!model?.reasoningEfforts?.some((candidate) => candidate.id === effort)) throw new QoderLlmError(`Qoder model "${options.model}" does not advertise reasoning effort "${effort}".`, "UNSUPPORTED_REASONING_EFFORT");
|
|
565
1007
|
}
|
|
566
|
-
|
|
1008
|
+
if (options.messages.some((message) => contentHasImage(message.content)) && model?.supportsImages !== true) throw new QoderLlmError(`Qoder model "${options.model}" does not advertise image input.`, "UNSUPPORTED_CONTENT");
|
|
1009
|
+
validateMessageShapes(options.messages);
|
|
1010
|
+
}
|
|
1011
|
+
/** Translate a request whose shape has already been validated. */
|
|
1012
|
+
function translateQoderMessages(options, attachments, pipeline) {
|
|
1013
|
+
return validateAndTranslateMessages(options.messages, options.system, attachments, options.signal, pipeline);
|
|
1014
|
+
}
|
|
1015
|
+
async function validateQoderRequest(options, model, attachments) {
|
|
1016
|
+
validateQoderRequestShape(options, model);
|
|
1017
|
+
return validateAndTranslateMessages(options.messages, options.system, attachments, options.signal);
|
|
567
1018
|
}
|
|
568
|
-
function buildQoderRequestBody(options, userId, translatedMessages, model) {
|
|
1019
|
+
async function buildQoderRequestBody(options, userId, translatedMessages, model, attachments) {
|
|
569
1020
|
if (!userId) throw new QoderLlmError("Qoder request identity is missing.", "AUTH");
|
|
570
1021
|
const modelKey = options.model || "cmodel";
|
|
571
|
-
const messages = translatedMessages ?? validateQoderRequest(options, model);
|
|
1022
|
+
const messages = translatedMessages ?? await validateQoderRequest(options, model, attachments);
|
|
572
1023
|
const modelMaxTokens = model?.maxTokens ?? 32768;
|
|
573
1024
|
const maxTokens = Math.min(options.maxTokens ?? modelMaxTokens, modelMaxTokens);
|
|
574
|
-
const isReasoning = model?.isReasoning ?? false;
|
|
1025
|
+
const isReasoning = options.reasoningEffort !== void 0 || (model?.isReasoning ?? false);
|
|
575
1026
|
const tools = translateTools(options.tools);
|
|
576
|
-
const
|
|
1027
|
+
const defaultContexts = Object.values(model?.contextOptions ?? {}).filter((option) => option.isDefault === true && typeof option.tokenCount === "number" && Number.isFinite(option.tokenCount) && option.tokenCount > 0);
|
|
1028
|
+
const contextConfig = model?.contextOptions === void 0 || defaultContexts.length !== 1 ? void 0 : Object.fromEntries(Object.entries(model.contextOptions).map(([key, value]) => [key, {
|
|
577
1029
|
...value.tokenCount === void 0 ? {} : { token_count: value.tokenCount },
|
|
578
1030
|
...value.isDefault === void 0 ? {} : { is_default: value.isDefault }
|
|
579
1031
|
}]));
|
|
580
1032
|
let lastUserText = "";
|
|
581
1033
|
for (let index = messages.length - 1; index >= 0; index--) if (messages[index].role === "user") {
|
|
582
|
-
|
|
1034
|
+
const content = messages[index].content;
|
|
1035
|
+
lastUserText = typeof content === "string" ? content : Array.isArray(content) ? content.filter((part) => part.type === "text").map((part) => part.text).join("") : "";
|
|
583
1036
|
break;
|
|
584
1037
|
}
|
|
585
1038
|
const stablePart = stableHash("qoder-session", userId, modelKey);
|
|
@@ -640,10 +1093,570 @@ function buildQoderRequestBody(options, userId, translatedMessages, model) {
|
|
|
640
1093
|
name: lastUserText.substring(0, 30),
|
|
641
1094
|
begin_at: Date.now()
|
|
642
1095
|
}
|
|
643
|
-
};
|
|
1096
|
+
};
|
|
1097
|
+
}
|
|
1098
|
+
const mediaTypeExtensions = {
|
|
1099
|
+
"image/png": "png",
|
|
1100
|
+
"image/jpeg": "jpg",
|
|
1101
|
+
"image/webp": "webp",
|
|
1102
|
+
"image/gif": "gif"
|
|
1103
|
+
};
|
|
1104
|
+
function aborted$2() {
|
|
1105
|
+
return new QoderLlmError("Qoder image upload was aborted.", "ABORTED");
|
|
1106
|
+
}
|
|
1107
|
+
/** Encode one image as a single-field multipart payload using the Qoder boundary shape. */
|
|
1108
|
+
function buildQoderImageMultipart(data, mediaType, boundaryId = crypto.randomUUID()) {
|
|
1109
|
+
const boundary = `----qodercli-${boundaryId}`;
|
|
1110
|
+
const extension = mediaTypeExtensions[mediaType] ?? "png";
|
|
1111
|
+
const header = Buffer.from(`--${boundary}\r\nContent-Disposition: form-data; name="file"; filename="image.${extension}"\r\nContent-Type: ${mediaType}\r\n\r\n`, "utf8");
|
|
1112
|
+
const footer = Buffer.from(`\r\n--${boundary}--\r\n`, "utf8");
|
|
1113
|
+
return {
|
|
1114
|
+
body: Buffer.concat([
|
|
1115
|
+
header,
|
|
1116
|
+
Buffer.from(data),
|
|
1117
|
+
footer
|
|
1118
|
+
]),
|
|
1119
|
+
boundary
|
|
1120
|
+
};
|
|
1121
|
+
}
|
|
1122
|
+
/**
|
|
1123
|
+
* Extract the durable object URL from a center upload response.
|
|
1124
|
+
*
|
|
1125
|
+
* @param payload - parsed response body.
|
|
1126
|
+
* @returns the first usable absolute URL, or undefined when none is present.
|
|
1127
|
+
*/
|
|
1128
|
+
function readQoderImageUrl(payload) {
|
|
1129
|
+
if (payload === null || typeof payload !== "object") return void 0;
|
|
1130
|
+
const root = payload;
|
|
1131
|
+
const result = root.result;
|
|
1132
|
+
const data = root.data;
|
|
1133
|
+
const candidates = [
|
|
1134
|
+
root.url,
|
|
1135
|
+
result?.url,
|
|
1136
|
+
result?.oss_url,
|
|
1137
|
+
data?.url,
|
|
1138
|
+
data?.oss_url
|
|
1139
|
+
];
|
|
1140
|
+
for (const candidate of candidates) {
|
|
1141
|
+
if (typeof candidate !== "string") continue;
|
|
1142
|
+
const trimmed = candidate.trim();
|
|
1143
|
+
if (trimmed.length === 0) continue;
|
|
1144
|
+
if (!URL.canParse(trimmed)) continue;
|
|
1145
|
+
return trimmed;
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
function dataUrl(image) {
|
|
1149
|
+
return `data:${image.mediaType};base64,${Buffer.from(image.data).toString("base64")}`;
|
|
1150
|
+
}
|
|
1151
|
+
/**
|
|
1152
|
+
* Publishes request images to the Qoder center service and remembers the result.
|
|
1153
|
+
*
|
|
1154
|
+
* Publication never fails a model request: any upstream problem degrades to an
|
|
1155
|
+
* inline data URL so the turn proceeds with the same content it would have
|
|
1156
|
+
* carried before the pipeline existed.
|
|
1157
|
+
*/
|
|
1158
|
+
var QoderImageUploader = class {
|
|
1159
|
+
cache = /* @__PURE__ */ new Map();
|
|
1160
|
+
flights = new SingleFlight();
|
|
1161
|
+
fetchImpl;
|
|
1162
|
+
region;
|
|
1163
|
+
logger;
|
|
1164
|
+
timeoutMs;
|
|
1165
|
+
cacheTtlMs;
|
|
1166
|
+
cacheCapacity;
|
|
1167
|
+
maxConcurrency;
|
|
1168
|
+
refreshCredentials;
|
|
1169
|
+
now;
|
|
1170
|
+
active = 0;
|
|
1171
|
+
queue = [];
|
|
1172
|
+
constructor(options = {}) {
|
|
1173
|
+
this.fetchImpl = options.fetch ?? globalThis.fetch;
|
|
1174
|
+
this.region = options.region ?? "global";
|
|
1175
|
+
this.logger = options.logger;
|
|
1176
|
+
this.timeoutMs = options.timeoutMs ?? 3e4;
|
|
1177
|
+
this.cacheTtlMs = options.cacheTtlMs ?? 18e5;
|
|
1178
|
+
this.cacheCapacity = options.cacheCapacity ?? 512;
|
|
1179
|
+
this.maxConcurrency = options.maxConcurrency ?? 4;
|
|
1180
|
+
this.refreshCredentials = options.refreshCredentials;
|
|
1181
|
+
this.now = options.now ?? Date.now;
|
|
1182
|
+
}
|
|
1183
|
+
/**
|
|
1184
|
+
* Resolve the wire URL for one request image.
|
|
1185
|
+
*
|
|
1186
|
+
* @param image - request-encoded image produced by the DSH attachment store.
|
|
1187
|
+
* @param credentials - credentials authorizing the center exchange.
|
|
1188
|
+
* @param signal - optional caller cancellation.
|
|
1189
|
+
* @returns a center object URL, or an inline data URL when publication fails.
|
|
1190
|
+
* @throws QoderLlmError with code `ABORTED` only when the caller cancels.
|
|
1191
|
+
*/
|
|
1192
|
+
async resolveImageUrl(image, credentials, signal) {
|
|
1193
|
+
if (signal?.aborted) throw aborted$2();
|
|
1194
|
+
const key = this.cacheKey(image, credentials);
|
|
1195
|
+
const cached = this.cache.get(key);
|
|
1196
|
+
if (cached !== void 0) {
|
|
1197
|
+
if (cached.expiresAt > this.now()) {
|
|
1198
|
+
this.cache.delete(key);
|
|
1199
|
+
this.cache.set(key, cached);
|
|
1200
|
+
this.logger?.debug?.("[image-upload] cache hit", {
|
|
1201
|
+
url: cached.url,
|
|
1202
|
+
region: this.region,
|
|
1203
|
+
mediaType: image.mediaType,
|
|
1204
|
+
bytes: image.data.byteLength
|
|
1205
|
+
});
|
|
1206
|
+
return cached.url;
|
|
1207
|
+
}
|
|
1208
|
+
this.cache.delete(key);
|
|
1209
|
+
}
|
|
1210
|
+
return this.flights.run(key, signal, async (sharedSignal) => {
|
|
1211
|
+
const url = await this.publish(image, credentials, sharedSignal);
|
|
1212
|
+
if (url !== void 0) {
|
|
1213
|
+
this.remember(key, url);
|
|
1214
|
+
return url;
|
|
1215
|
+
}
|
|
1216
|
+
return dataUrl(image);
|
|
1217
|
+
}, aborted$2);
|
|
1218
|
+
}
|
|
1219
|
+
/**
|
|
1220
|
+
* Derive the deduplication identity for one request image.
|
|
1221
|
+
*
|
|
1222
|
+
* `variantId` is the attachment store's deterministic cache and upload index
|
|
1223
|
+
* over the attachment, the request policy, and the encoder parameters, so it
|
|
1224
|
+
* identifies these exact bytes without rehashing them. Region and subscriber
|
|
1225
|
+
* are included because a center object is never shared across either.
|
|
1226
|
+
*/
|
|
1227
|
+
cacheKey(image, credentials) {
|
|
1228
|
+
const hash = crypto.createHash("sha256");
|
|
1229
|
+
hash.update(getQoderImageUploadUrl(this.region));
|
|
1230
|
+
hash.update("\0");
|
|
1231
|
+
hash.update(credentials.userID);
|
|
1232
|
+
hash.update("\0");
|
|
1233
|
+
hash.update(image.mediaType);
|
|
1234
|
+
hash.update("\0");
|
|
1235
|
+
const variantId = typeof image.variantId === "string" ? image.variantId : "";
|
|
1236
|
+
if (variantId.length > 0) hash.update(variantId);
|
|
1237
|
+
else hash.update(crypto.createHash("sha256").update(Buffer.from(image.data)).digest("hex"));
|
|
1238
|
+
return hash.digest("hex");
|
|
1239
|
+
}
|
|
1240
|
+
remember(key, url) {
|
|
1241
|
+
this.cache.delete(key);
|
|
1242
|
+
this.cache.set(key, {
|
|
1243
|
+
url,
|
|
1244
|
+
expiresAt: this.now() + this.cacheTtlMs
|
|
1245
|
+
});
|
|
1246
|
+
while (this.cache.size > this.cacheCapacity) {
|
|
1247
|
+
const oldest = this.cache.keys().next();
|
|
1248
|
+
if (oldest.done === true) break;
|
|
1249
|
+
this.cache.delete(oldest.value);
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
async withSlot(signal, operation) {
|
|
1253
|
+
if (signal.aborted) throw aborted$2();
|
|
1254
|
+
if (this.active >= this.maxConcurrency) {
|
|
1255
|
+
this.logger?.debug?.("[image-upload] queued", {
|
|
1256
|
+
region: this.region,
|
|
1257
|
+
active: this.active,
|
|
1258
|
+
queued: this.queue.length + 1
|
|
1259
|
+
});
|
|
1260
|
+
await new Promise((resolve, reject) => {
|
|
1261
|
+
const onAbort = () => {
|
|
1262
|
+
const index = this.queue.indexOf(release);
|
|
1263
|
+
if (index !== -1) this.queue.splice(index, 1);
|
|
1264
|
+
reject(aborted$2());
|
|
1265
|
+
};
|
|
1266
|
+
const release = () => {
|
|
1267
|
+
signal.removeEventListener("abort", onAbort);
|
|
1268
|
+
resolve();
|
|
1269
|
+
};
|
|
1270
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
1271
|
+
this.queue.push(release);
|
|
1272
|
+
});
|
|
1273
|
+
} else this.active++;
|
|
1274
|
+
try {
|
|
1275
|
+
if (signal.aborted) throw aborted$2();
|
|
1276
|
+
return await operation();
|
|
1277
|
+
} finally {
|
|
1278
|
+
const next = this.queue.shift();
|
|
1279
|
+
if (next !== void 0) next();
|
|
1280
|
+
else this.active--;
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
/** Attempt publication, returning undefined when the request must degrade. */
|
|
1284
|
+
async publish(image, credentials, signal) {
|
|
1285
|
+
return this.withSlot(signal, async () => {
|
|
1286
|
+
const first = await this.attempt(image, credentials, signal);
|
|
1287
|
+
if (first.url !== void 0) return first.url;
|
|
1288
|
+
if (!first.retryable || this.refreshCredentials === void 0) {
|
|
1289
|
+
this.warnDegraded(first.reason);
|
|
1290
|
+
return;
|
|
1291
|
+
}
|
|
1292
|
+
let refreshed;
|
|
1293
|
+
this.logger?.debug?.("[image-upload] refreshing credentials before retry", {
|
|
1294
|
+
region: this.region,
|
|
1295
|
+
reason: first.reason
|
|
1296
|
+
});
|
|
1297
|
+
try {
|
|
1298
|
+
refreshed = await this.refreshCredentials(signal);
|
|
1299
|
+
} catch (error) {
|
|
1300
|
+
if (signal.aborted) throw aborted$2();
|
|
1301
|
+
this.warnDegraded("credential refresh failed", error);
|
|
1302
|
+
return;
|
|
1303
|
+
}
|
|
1304
|
+
const second = await this.attempt(image, refreshed, signal);
|
|
1305
|
+
if (second.url !== void 0) return second.url;
|
|
1306
|
+
this.warnDegraded(second.reason);
|
|
1307
|
+
});
|
|
1308
|
+
}
|
|
1309
|
+
warnDegraded(reason, cause) {
|
|
1310
|
+
this.logger?.warn?.("[image-upload] upload failed, keeping base64 image", {
|
|
1311
|
+
region: this.region,
|
|
1312
|
+
reason
|
|
1313
|
+
}, ...cause === void 0 ? [] : [redactLogValue(cause)]);
|
|
1314
|
+
}
|
|
1315
|
+
async attempt(image, credentials, signal) {
|
|
1316
|
+
const requestId = crypto.randomUUID();
|
|
1317
|
+
const url = getQoderImageUploadUrl(this.region, requestId);
|
|
1318
|
+
const multipart = buildQoderImageMultipart(image.data, image.mediaType);
|
|
1319
|
+
const signedBody = Buffer.from(String(multipart.body.length), "utf8");
|
|
1320
|
+
const timeout = AbortSignal.timeout(this.timeoutMs);
|
|
1321
|
+
const requestSignal = AbortSignal.any([signal, timeout]);
|
|
1322
|
+
const startedAt = this.now();
|
|
1323
|
+
const details = {
|
|
1324
|
+
requestId,
|
|
1325
|
+
region: this.region,
|
|
1326
|
+
mediaType: image.mediaType,
|
|
1327
|
+
bytes: image.data.byteLength,
|
|
1328
|
+
timeoutMs: this.timeoutMs
|
|
1329
|
+
};
|
|
1330
|
+
this.logger?.debug?.("[image-upload] started", details);
|
|
1331
|
+
try {
|
|
1332
|
+
const response = await this.fetchImpl(url, {
|
|
1333
|
+
method: "PUT",
|
|
1334
|
+
headers: {
|
|
1335
|
+
...buildAuthHeaders(signedBody, url, credentials),
|
|
1336
|
+
"AI-CLIENT-TIMESTAMP": String(Math.floor(this.now() / 1e3)),
|
|
1337
|
+
"Content-Type": `multipart/form-data; boundary=${multipart.boundary}`,
|
|
1338
|
+
"Content-Length": String(multipart.body.length),
|
|
1339
|
+
"accept": "application/json"
|
|
1340
|
+
},
|
|
1341
|
+
body: new Uint8Array(multipart.body),
|
|
1342
|
+
signal: requestSignal
|
|
1343
|
+
});
|
|
1344
|
+
this.logger?.debug?.("[image-upload] response received", {
|
|
1345
|
+
requestId,
|
|
1346
|
+
status: response.status,
|
|
1347
|
+
elapsedMs: this.now() - startedAt
|
|
1348
|
+
});
|
|
1349
|
+
const text = await readLimitedText(response, defaultMaxJsonBytes, "Qoder image upload response");
|
|
1350
|
+
if (!response.ok) return {
|
|
1351
|
+
retryable: response.status === 401 || response.status === 403,
|
|
1352
|
+
reason: `HTTP ${response.status}`
|
|
1353
|
+
};
|
|
1354
|
+
let payload;
|
|
1355
|
+
try {
|
|
1356
|
+
payload = JSON.parse(text);
|
|
1357
|
+
} catch {
|
|
1358
|
+
return {
|
|
1359
|
+
retryable: false,
|
|
1360
|
+
reason: "invalid JSON response"
|
|
1361
|
+
};
|
|
1362
|
+
}
|
|
1363
|
+
const resolved = readQoderImageUrl(payload);
|
|
1364
|
+
if (resolved === void 0) return {
|
|
1365
|
+
retryable: false,
|
|
1366
|
+
reason: "response carried no image URL"
|
|
1367
|
+
};
|
|
1368
|
+
this.logger?.debug?.("[image-upload] succeeded", {
|
|
1369
|
+
...details,
|
|
1370
|
+
status: response.status,
|
|
1371
|
+
elapsedMs: this.now() - startedAt,
|
|
1372
|
+
url: resolved
|
|
1373
|
+
});
|
|
1374
|
+
return {
|
|
1375
|
+
url: resolved,
|
|
1376
|
+
retryable: false,
|
|
1377
|
+
reason: ""
|
|
1378
|
+
};
|
|
1379
|
+
} catch (error) {
|
|
1380
|
+
const failureDetails = {
|
|
1381
|
+
requestId,
|
|
1382
|
+
region: this.region,
|
|
1383
|
+
elapsedMs: this.now() - startedAt
|
|
1384
|
+
};
|
|
1385
|
+
if (signal.aborted) {
|
|
1386
|
+
this.logger?.debug?.("[image-upload] aborted", failureDetails);
|
|
1387
|
+
throw aborted$2();
|
|
1388
|
+
}
|
|
1389
|
+
if (timeout.aborted) {
|
|
1390
|
+
this.logger?.debug?.("[image-upload] timed out", failureDetails);
|
|
1391
|
+
return {
|
|
1392
|
+
retryable: false,
|
|
1393
|
+
reason: "upload timed out"
|
|
1394
|
+
};
|
|
1395
|
+
}
|
|
1396
|
+
this.logger?.debug?.("[image-upload] network failure", failureDetails, redactLogValue(error));
|
|
1397
|
+
return {
|
|
1398
|
+
retryable: false,
|
|
1399
|
+
reason: "network failure"
|
|
1400
|
+
};
|
|
1401
|
+
}
|
|
1402
|
+
}
|
|
1403
|
+
};
|
|
1404
|
+
//#endregion
|
|
1405
|
+
//#region src/qoder/transport/account-reader.ts
|
|
1406
|
+
const defaultUsageTtlMs = 6e4;
|
|
1407
|
+
const defaultUsageTimeoutMs = 15e3;
|
|
1408
|
+
function normalizeQuota(raw) {
|
|
1409
|
+
if (!raw || typeof raw !== "object") return void 0;
|
|
1410
|
+
const total = typeof raw.total === "number" && Number.isFinite(raw.total) ? raw.total : typeof raw.cap === "number" && Number.isFinite(raw.cap) ? raw.cap : typeof raw.remaining === "number" && typeof raw.used === "number" ? raw.used + raw.remaining : 0;
|
|
1411
|
+
const used = typeof raw.used === "number" && Number.isFinite(raw.used) ? raw.used : 0;
|
|
1412
|
+
const remaining = typeof raw.remaining === "number" && Number.isFinite(raw.remaining) ? raw.remaining : Math.max(0, total - used);
|
|
1413
|
+
let percentage;
|
|
1414
|
+
if (typeof raw.percentage === "number" && Number.isFinite(raw.percentage)) percentage = raw.percentage <= 1 && total > 1 ? raw.percentage * 100 : raw.percentage;
|
|
1415
|
+
else percentage = total > 0 ? used / total * 100 : 0;
|
|
1416
|
+
const unit = typeof raw.unit === "string" && raw.unit.length > 0 ? raw.unit : "credits";
|
|
1417
|
+
return {
|
|
1418
|
+
total,
|
|
1419
|
+
used,
|
|
1420
|
+
remaining,
|
|
1421
|
+
percentage,
|
|
1422
|
+
unit
|
|
1423
|
+
};
|
|
1424
|
+
}
|
|
1425
|
+
function normalizeExpiresAt(rawExpires) {
|
|
1426
|
+
if (rawExpires === void 0 || rawExpires === null) return void 0;
|
|
1427
|
+
if (typeof rawExpires === "number" && rawExpires > 0) return new Date(rawExpires).toISOString();
|
|
1428
|
+
if (typeof rawExpires === "string" && rawExpires.length > 0) {
|
|
1429
|
+
const parsed = Date.parse(rawExpires);
|
|
1430
|
+
if (!Number.isNaN(parsed) && parsed > 0) return new Date(parsed).toISOString();
|
|
1431
|
+
}
|
|
1432
|
+
}
|
|
1433
|
+
function asString(value) {
|
|
1434
|
+
return typeof value === "string" && value.trim().length > 0 ? value.trim() : void 0;
|
|
1435
|
+
}
|
|
1436
|
+
function asBoolean(value) {
|
|
1437
|
+
return typeof value === "boolean" ? value : void 0;
|
|
1438
|
+
}
|
|
1439
|
+
function asNumber(value) {
|
|
1440
|
+
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
1441
|
+
if (typeof value === "string" && value.trim().length > 0) {
|
|
1442
|
+
const num = Number(value);
|
|
1443
|
+
if (Number.isFinite(num)) return num;
|
|
1444
|
+
}
|
|
1445
|
+
}
|
|
1446
|
+
function normalizeOrganization(raw) {
|
|
1447
|
+
if (!raw || typeof raw !== "object") return void 0;
|
|
1448
|
+
const obj = raw;
|
|
1449
|
+
const orgId = asString(obj.org_id) ?? asString(obj.orgId) ?? asString(obj.id);
|
|
1450
|
+
const orgName = asString(obj.org_name) ?? asString(obj.orgName) ?? asString(obj.name);
|
|
1451
|
+
if (!orgId || !orgName) return void 0;
|
|
1452
|
+
return {
|
|
1453
|
+
orgId,
|
|
1454
|
+
orgName,
|
|
1455
|
+
...asString(obj.role_name) ?? asString(obj.roleName) !== void 0 ? { roleName: asString(obj.role_name) ?? asString(obj.roleName) } : {},
|
|
1456
|
+
isSuspended: asBoolean(obj.is_suspended) ?? asBoolean(obj.isSuspended) ?? false,
|
|
1457
|
+
canManageSubscriptions: asBoolean(obj.can_manage_subscriptions) ?? asBoolean(obj.canManageSubscriptions) ?? false,
|
|
1458
|
+
resourcePackageFeatureEnabled: asBoolean(obj.resource_package_feature_enabled) ?? asBoolean(obj.resourcePackageFeatureEnabled) ?? false
|
|
1459
|
+
};
|
|
1460
|
+
}
|
|
1461
|
+
function normalizeFeatureAllowed(raw) {
|
|
1462
|
+
if (!raw || typeof raw !== "object") return void 0;
|
|
1463
|
+
const obj = raw;
|
|
1464
|
+
return {
|
|
1465
|
+
quest: asBoolean(obj.quest) ?? false,
|
|
1466
|
+
wiki: asBoolean(obj.wiki) ?? false,
|
|
1467
|
+
codeReview: asBoolean(obj.code_review) ?? asBoolean(obj.codeReview) ?? false
|
|
1468
|
+
};
|
|
1469
|
+
}
|
|
1470
|
+
function normalizePlan(raw) {
|
|
1471
|
+
if (!raw || typeof raw !== "object") return void 0;
|
|
1472
|
+
const obj = raw;
|
|
1473
|
+
const userType = asString(obj.user_type) ?? asString(obj.userType);
|
|
1474
|
+
const planTierName = asString(obj.plan_tier_name) ?? asString(obj.planTierName) ?? asString(obj.plan_name) ?? asString(obj.planName);
|
|
1475
|
+
if (!userType || !planTierName) return void 0;
|
|
1476
|
+
const organization = normalizeOrganization(obj.organization);
|
|
1477
|
+
const isPersonalVersion = asBoolean(obj.is_personal_version) ?? asBoolean(obj.isPersonalVersion) ?? organization === void 0;
|
|
1478
|
+
const startDate = normalizeExpiresAt(obj.start_date ?? obj.startDate);
|
|
1479
|
+
const endDate = normalizeExpiresAt(obj.end_date ?? obj.endDate);
|
|
1480
|
+
const planTier = asString(obj.plan_tier) ?? asString(obj.planTier);
|
|
1481
|
+
const isHighestTier = asBoolean(obj.is_highest_tier) ?? asBoolean(obj.isHighestTier);
|
|
1482
|
+
const isRenewed = asBoolean(obj.is_renewed) ?? asBoolean(obj.isRenewed);
|
|
1483
|
+
const featureAllowed = normalizeFeatureAllowed(obj.feature_allowed ?? obj.featureAllowed);
|
|
1484
|
+
return {
|
|
1485
|
+
userType,
|
|
1486
|
+
planTierName,
|
|
1487
|
+
...planTier !== void 0 ? { planTier } : {},
|
|
1488
|
+
isPersonalVersion,
|
|
1489
|
+
...isHighestTier !== void 0 ? { isHighestTier } : {},
|
|
1490
|
+
...isRenewed !== void 0 ? { isRenewed } : {},
|
|
1491
|
+
...startDate !== void 0 ? { startDate } : {},
|
|
1492
|
+
...endDate !== void 0 ? { endDate } : {},
|
|
1493
|
+
...organization !== void 0 ? { organization } : {},
|
|
1494
|
+
...featureAllowed !== void 0 ? { featureAllowed } : {},
|
|
1495
|
+
raw
|
|
1496
|
+
};
|
|
1497
|
+
}
|
|
1498
|
+
function normalizeStatus(raw) {
|
|
1499
|
+
if (!raw || typeof raw !== "object") return void 0;
|
|
1500
|
+
const obj = raw;
|
|
1501
|
+
const featureSwitches = obj.featureSwitches ?? obj.feature_switches;
|
|
1502
|
+
const teamSwitches = obj.teamSwitches ?? obj.team_switches;
|
|
1503
|
+
const allowByok = asNumber(featureSwitches?.allow_byok ?? featureSwitches?.allowByok) ?? 0;
|
|
1504
|
+
const teamAllowByok = asNumber(teamSwitches?.allow_byok ?? teamSwitches?.allowByok);
|
|
1505
|
+
const isPrivacyPolicyModifiable = asBoolean(obj.isPrivacyPolicyModifiable ?? obj.is_data_policy_modifiable);
|
|
1506
|
+
return {
|
|
1507
|
+
allowByok,
|
|
1508
|
+
...teamAllowByok !== void 0 ? { teamAllowByok } : {},
|
|
1509
|
+
...isPrivacyPolicyModifiable !== void 0 ? { isPrivacyPolicyModifiable } : {},
|
|
1510
|
+
raw
|
|
1511
|
+
};
|
|
1512
|
+
}
|
|
1513
|
+
var QoderUsageReader = class {
|
|
1514
|
+
authService;
|
|
1515
|
+
fetchImpl;
|
|
1516
|
+
ttlMs;
|
|
1517
|
+
timeoutMs;
|
|
1518
|
+
region;
|
|
1519
|
+
logger;
|
|
1520
|
+
cache = /* @__PURE__ */ new Map();
|
|
1521
|
+
flights = new SingleFlight();
|
|
1522
|
+
constructor(options) {
|
|
1523
|
+
this.authService = options.authService;
|
|
1524
|
+
this.fetchImpl = options.fetch ?? globalThis.fetch;
|
|
1525
|
+
this.ttlMs = options.ttlMs ?? defaultUsageTtlMs;
|
|
1526
|
+
this.timeoutMs = options.timeoutMs ?? defaultUsageTimeoutMs;
|
|
1527
|
+
this.region = options.region ?? "global";
|
|
1528
|
+
this.logger = options.logger;
|
|
1529
|
+
}
|
|
1530
|
+
async readAccount(pat, options) {
|
|
1531
|
+
if (!pat || typeof pat !== "string") throw new QoderLlmError("Qoder Personal Access Token is missing or invalid.", "MISSING_CREDENTIAL");
|
|
1532
|
+
const cacheKey = `${this.region}:${opaqueCredentialKey(pat)}`;
|
|
1533
|
+
if (!options?.force) {
|
|
1534
|
+
const cached = this.cache.get(cacheKey);
|
|
1535
|
+
if (cached && cached.expiresAt > Date.now()) return cached.info;
|
|
1536
|
+
}
|
|
1537
|
+
return this.flights.run(cacheKey, options?.signal, (sharedSignal) => this.loadAccount(pat, sharedSignal, cacheKey), () => new QoderLlmError("Qoder account request was aborted.", "ABORTED"));
|
|
1538
|
+
}
|
|
1539
|
+
async loadAccount(pat, signal, cacheKey) {
|
|
1540
|
+
const creds = await this.authService.getCredentials(pat, signal);
|
|
1541
|
+
const profile = {
|
|
1542
|
+
id: creds.userID,
|
|
1543
|
+
name: creds.name || "Qoder User",
|
|
1544
|
+
email: creds.email || ""
|
|
1545
|
+
};
|
|
1546
|
+
const [usage, plan, status] = await Promise.all([
|
|
1547
|
+
retryMetadataRead(signal, () => this.fetchUsage(creds.authToken, signal)),
|
|
1548
|
+
this.safeFetchPlan(creds.authToken, signal),
|
|
1549
|
+
this.safeFetchStatus(creds.authToken, creds.machineID, signal)
|
|
1550
|
+
]);
|
|
1551
|
+
const accountInfo = {
|
|
1552
|
+
profile,
|
|
1553
|
+
usage,
|
|
1554
|
+
...plan !== void 0 ? { plan } : {},
|
|
1555
|
+
...status !== void 0 ? { status } : {},
|
|
1556
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1557
|
+
};
|
|
1558
|
+
this.cache.set(cacheKey, {
|
|
1559
|
+
info: accountInfo,
|
|
1560
|
+
expiresAt: Date.now() + this.ttlMs
|
|
1561
|
+
});
|
|
1562
|
+
return accountInfo;
|
|
1563
|
+
}
|
|
1564
|
+
clear(pat) {
|
|
1565
|
+
if (pat) this.cache.delete(`${this.region}:${opaqueCredentialKey(pat)}`);
|
|
1566
|
+
else this.cache.clear();
|
|
1567
|
+
}
|
|
1568
|
+
async fetchUsage(jobToken, signal) {
|
|
1569
|
+
const data = await openApiJsonRequest(this.fetchImpl, {
|
|
1570
|
+
url: getQoderUsageUrl(this.region),
|
|
1571
|
+
token: jobToken,
|
|
1572
|
+
signal,
|
|
1573
|
+
timeoutMs: this.timeoutMs,
|
|
1574
|
+
logger: this.logger,
|
|
1575
|
+
operation: "Usage",
|
|
1576
|
+
logCategory: "account.usage"
|
|
1577
|
+
});
|
|
1578
|
+
return {
|
|
1579
|
+
userQuota: normalizeQuota(data.userQuota),
|
|
1580
|
+
orgResourcePackage: normalizeQuota(data.orgResourcePackage),
|
|
1581
|
+
totalUsagePercentage: typeof data.totalUsagePercentage === "number" ? data.totalUsagePercentage : void 0,
|
|
1582
|
+
isQuotaExceeded: typeof data.isQuotaExceeded === "boolean" ? data.isQuotaExceeded : false,
|
|
1583
|
+
expiresAt: normalizeExpiresAt(data.expiresAt),
|
|
1584
|
+
raw: data
|
|
1585
|
+
};
|
|
1586
|
+
}
|
|
1587
|
+
async fetchPlan(jobToken, signal) {
|
|
1588
|
+
return normalizePlan(await openApiJsonRequest(this.fetchImpl, {
|
|
1589
|
+
url: getQoderUserPlanUrl(this.region),
|
|
1590
|
+
token: jobToken,
|
|
1591
|
+
signal,
|
|
1592
|
+
timeoutMs: this.timeoutMs,
|
|
1593
|
+
logger: this.logger,
|
|
1594
|
+
operation: "Plan",
|
|
1595
|
+
logCategory: "account.plan"
|
|
1596
|
+
}));
|
|
1597
|
+
}
|
|
1598
|
+
async safeFetchPlan(jobToken, signal) {
|
|
1599
|
+
try {
|
|
1600
|
+
return await this.fetchPlan(jobToken, signal);
|
|
1601
|
+
} catch (error) {
|
|
1602
|
+
if (signal.aborted) throw error;
|
|
1603
|
+
this.logger?.warn?.("[Qoder Plan] Failed to load user plan (degraded)", error instanceof Error ? error.message : error);
|
|
1604
|
+
return;
|
|
1605
|
+
}
|
|
1606
|
+
}
|
|
1607
|
+
async fetchStatus(jobToken, machineId, signal) {
|
|
1608
|
+
return normalizeStatus(await openApiJsonRequest(this.fetchImpl, {
|
|
1609
|
+
url: getQoderUserStatusUrl(this.region),
|
|
1610
|
+
token: jobToken,
|
|
1611
|
+
machineId,
|
|
1612
|
+
signal,
|
|
1613
|
+
timeoutMs: this.timeoutMs,
|
|
1614
|
+
logger: this.logger,
|
|
1615
|
+
operation: "Status",
|
|
1616
|
+
logCategory: "account.status"
|
|
1617
|
+
}));
|
|
1618
|
+
}
|
|
1619
|
+
async safeFetchStatus(jobToken, machineId, signal) {
|
|
1620
|
+
try {
|
|
1621
|
+
return await this.fetchStatus(jobToken, machineId, signal);
|
|
1622
|
+
} catch (error) {
|
|
1623
|
+
if (signal?.aborted) throw error;
|
|
1624
|
+
this.logger?.warn?.("[Qoder Status] Failed to load user status (degraded)", error instanceof Error ? error.message : error);
|
|
1625
|
+
return;
|
|
1626
|
+
}
|
|
1627
|
+
}
|
|
1628
|
+
};
|
|
1629
|
+
//#endregion
|
|
1630
|
+
//#region src/qoder/transport/wire/encoding.ts
|
|
1631
|
+
/**
|
|
1632
|
+
* Qoder WAF Body encoding algorithm.
|
|
1633
|
+
*
|
|
1634
|
+
* Re-arranges standard Base64 chunks and translates characters against
|
|
1635
|
+
* a custom alphabet to satisfy Qoder upstream WAF requirements.
|
|
1636
|
+
*
|
|
1637
|
+
* @module dsh-provider-qoder/qoder/transport/wire/encoding
|
|
1638
|
+
*/
|
|
1639
|
+
const qoderCustomAlphabet = "_doRTgHZBKcGVjlvpC,@aFSx#DPuNJme&i*MzLOEn)sUrthbf%Y^w.(kIQyXqWA!";
|
|
1640
|
+
const qoderStdAlphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
1641
|
+
function qoderEncodeBody(plaintext) {
|
|
1642
|
+
const std = Buffer.isBuffer(plaintext) ? plaintext.toString("base64") : Buffer.from(plaintext).toString("base64");
|
|
1643
|
+
const n = std.length;
|
|
1644
|
+
const a = Math.floor(n / 3);
|
|
1645
|
+
const rearranged = std.slice(n - a) + std.slice(a, n - a) + std.slice(0, a);
|
|
1646
|
+
let out = "";
|
|
1647
|
+
for (let i = 0; i < n; i++) {
|
|
1648
|
+
const c = rearranged[i];
|
|
1649
|
+
if (c === "=") out += "$";
|
|
1650
|
+
else {
|
|
1651
|
+
const idx = qoderStdAlphabet.indexOf(c);
|
|
1652
|
+
if (idx >= 0) out += qoderCustomAlphabet[idx];
|
|
1653
|
+
else out += c;
|
|
1654
|
+
}
|
|
1655
|
+
}
|
|
1656
|
+
return out;
|
|
644
1657
|
}
|
|
645
1658
|
//#endregion
|
|
646
|
-
//#region src/thinking.ts
|
|
1659
|
+
//#region src/qoder/transport/wire/thinking.ts
|
|
647
1660
|
const thinkingTags = [{
|
|
648
1661
|
open: "<thinking>",
|
|
649
1662
|
close: "</thinking>"
|
|
@@ -800,7 +1813,7 @@ var QoderThinkingParser = class {
|
|
|
800
1813
|
}
|
|
801
1814
|
};
|
|
802
1815
|
//#endregion
|
|
803
|
-
//#region src/sse.ts
|
|
1816
|
+
//#region src/qoder/transport/wire/sse.ts
|
|
804
1817
|
/** Decode Qoder's outer SSE envelope and inner model chunks. */
|
|
805
1818
|
const doneMarker = "[DONE]";
|
|
806
1819
|
function malformed(message) {
|
|
@@ -866,6 +1879,7 @@ async function* parseQoderSse(stream, options = {}) {
|
|
|
866
1879
|
let pendingUsage;
|
|
867
1880
|
let terminalKind = "stop";
|
|
868
1881
|
let sawContent = false;
|
|
1882
|
+
const maxBufferChars = options.maxBufferChars ?? 2097152;
|
|
869
1883
|
const closeTextual = () => {
|
|
870
1884
|
if (activeTextual === void 0) return [];
|
|
871
1885
|
const state = activeTextual;
|
|
@@ -942,6 +1956,7 @@ async function* parseQoderSse(stream, options = {}) {
|
|
|
942
1956
|
} else {
|
|
943
1957
|
options.onActivity?.();
|
|
944
1958
|
buffer += decoder.decode(value, { stream: true });
|
|
1959
|
+
if (buffer.length > maxBufferChars) throw malformed("Qoder SSE frame exceeded its size limit.");
|
|
945
1960
|
}
|
|
946
1961
|
while (!sawDone) {
|
|
947
1962
|
const lineEnd = buffer.indexOf("\n");
|
|
@@ -1071,531 +2086,217 @@ async function* parseQoderSse(stream, options = {}) {
|
|
|
1071
2086
|
reason: {
|
|
1072
2087
|
kind: "error",
|
|
1073
2088
|
failure: {
|
|
1074
|
-
message: "Model returned a completed response with no content.",
|
|
1075
|
-
code: EMPTY_RESPONSE_CODE
|
|
1076
|
-
}
|
|
1077
|
-
}
|
|
1078
|
-
};
|
|
1079
|
-
return;
|
|
1080
|
-
}
|
|
1081
|
-
yield {
|
|
1082
|
-
type: "finish",
|
|
1083
|
-
reason: { kind: toolCalls.size > 0 ? "tool-calls" : terminalKind }
|
|
1084
|
-
};
|
|
1085
|
-
}
|
|
1086
|
-
//#endregion
|
|
1087
|
-
//#region src/adapter.ts
|
|
1088
|
-
/** DSH LlmAdapter for the Global Qoder subscription transport. */
|
|
1089
|
-
const defaultMaxTokens = 32768;
|
|
1090
|
-
const defaultStreamIdleTimeoutMs = 300 * 1e3;
|
|
1091
|
-
const defaultModels = [
|
|
1092
|
-
{
|
|
1093
|
-
id: "cmodel",
|
|
1094
|
-
name: "Cantus (Qoder)",
|
|
1095
|
-
description: "Default Global Qoder subscription model for quick validation",
|
|
1096
|
-
contextWindow: 1e6,
|
|
1097
|
-
maxTokens: defaultMaxTokens
|
|
1098
|
-
},
|
|
1099
|
-
{
|
|
1100
|
-
id: "auto",
|
|
1101
|
-
name: "Qoder Auto",
|
|
1102
|
-
description: "Server-routed Global Qoder model pool",
|
|
1103
|
-
contextWindow: 18e4,
|
|
1104
|
-
maxTokens: defaultMaxTokens
|
|
1105
|
-
},
|
|
1106
|
-
{
|
|
1107
|
-
id: "ultimate",
|
|
1108
|
-
name: "Qoder Ultimate",
|
|
1109
|
-
description: "Highest-capability Global Qoder model pool",
|
|
1110
|
-
contextWindow: 1e6,
|
|
1111
|
-
maxTokens: defaultMaxTokens
|
|
1112
|
-
},
|
|
1113
|
-
{
|
|
1114
|
-
id: "performance",
|
|
1115
|
-
name: "Qoder Performance",
|
|
1116
|
-
description: "Performance-oriented Global Qoder model pool",
|
|
1117
|
-
contextWindow: 1e6,
|
|
1118
|
-
maxTokens: defaultMaxTokens
|
|
1119
|
-
},
|
|
1120
|
-
{
|
|
1121
|
-
id: "efficient",
|
|
1122
|
-
name: "Qoder Efficient",
|
|
1123
|
-
description: "Efficiency-oriented Global Qoder model pool",
|
|
1124
|
-
contextWindow: 18e4,
|
|
1125
|
-
maxTokens: defaultMaxTokens
|
|
1126
|
-
},
|
|
1127
|
-
{
|
|
1128
|
-
id: "lite",
|
|
1129
|
-
name: "Qoder Lite",
|
|
1130
|
-
description: "Basic Global Qoder model pool",
|
|
1131
|
-
contextWindow: 18e4,
|
|
1132
|
-
maxTokens: defaultMaxTokens
|
|
1133
|
-
}
|
|
1134
|
-
];
|
|
1135
|
-
function modelInfo(provider, model) {
|
|
1136
|
-
return {
|
|
1137
|
-
provider,
|
|
1138
|
-
id: model.id,
|
|
1139
|
-
name: model.priceFactor === void 0 ? model.name : `${model.name} (${model.priceFactor}x)`,
|
|
1140
|
-
description: model.description,
|
|
1141
|
-
inputModalities: ["text"]
|
|
1142
|
-
};
|
|
1143
|
-
}
|
|
1144
|
-
var QoderAdapter = class extends LlmAdapter {
|
|
1145
|
-
resolvePat;
|
|
1146
|
-
catalogModels;
|
|
1147
|
-
authService;
|
|
1148
|
-
fetchImpl;
|
|
1149
|
-
logger;
|
|
1150
|
-
streamIdleTimeoutMs;
|
|
1151
|
-
providerId;
|
|
1152
|
-
providerName;
|
|
1153
|
-
region;
|
|
1154
|
-
constructor(options) {
|
|
1155
|
-
super();
|
|
1156
|
-
this.resolvePat = options.resolvePat;
|
|
1157
|
-
this.catalogModels = options.models && options.models.length > 0 ? options.models : defaultModels;
|
|
1158
|
-
this.authService = options.authService ?? new QoderAuthService({ fetch: options.fetch });
|
|
1159
|
-
this.fetchImpl = options.fetch ?? globalThis.fetch;
|
|
1160
|
-
this.logger = options.logger;
|
|
1161
|
-
this.streamIdleTimeoutMs = options.streamIdleTimeoutMs ?? 3e5;
|
|
1162
|
-
this.providerId = options.providerId ?? "qoder-official";
|
|
1163
|
-
this.providerName = options.providerName ?? "Qoder";
|
|
1164
|
-
this.region = options.region ?? "global";
|
|
1165
|
-
}
|
|
1166
|
-
providerInfo(provider) {
|
|
1167
|
-
return {
|
|
1168
|
-
id: provider,
|
|
1169
|
-
name: this.providerName
|
|
1170
|
-
};
|
|
1171
|
-
}
|
|
1172
|
-
listModels(provider) {
|
|
1173
|
-
return Promise.resolve(this.catalogModels.map((model) => modelInfo(provider, model)));
|
|
1174
|
-
}
|
|
1175
|
-
replaceConfig(models, streamIdleTimeoutMs, region = "global") {
|
|
1176
|
-
this.catalogModels = models;
|
|
1177
|
-
this.streamIdleTimeoutMs = streamIdleTimeoutMs;
|
|
1178
|
-
this.region = region;
|
|
1179
|
-
}
|
|
1180
|
-
resolveModel(provider, modelId, signal) {
|
|
1181
|
-
if (signal?.aborted) return Promise.reject(new QoderLlmError("Qoder model resolution was aborted.", "ABORTED"));
|
|
1182
|
-
const configured = this.catalogModels.find((model) => model.id === modelId);
|
|
1183
|
-
if (configured === void 0) return Promise.resolve({
|
|
1184
|
-
provider,
|
|
1185
|
-
id: modelId,
|
|
1186
|
-
name: modelId,
|
|
1187
|
-
inputModalities: ["text"]
|
|
1188
|
-
});
|
|
1189
|
-
return Promise.resolve({
|
|
1190
|
-
...modelInfo(provider, configured),
|
|
1191
|
-
...configured.contextWindow === void 0 ? {} : { context: { contextWindow: configured.contextWindow } },
|
|
1192
|
-
...configured.maxTokens === void 0 ? {} : { defaultMaxTokens: configured.maxTokens },
|
|
1193
|
-
...configured.reasoningEfforts === void 0 || configured.reasoningEfforts.length === 0 ? {} : { reasoning: {
|
|
1194
|
-
efforts: configured.reasoningEfforts.map((effort) => ({
|
|
1195
|
-
id: ReasoningEffortId(effort.id),
|
|
1196
|
-
name: effort.name,
|
|
1197
|
-
...effort.description === void 0 ? {} : { description: effort.description }
|
|
1198
|
-
})),
|
|
1199
|
-
...configured.defaultReasoningEffort === void 0 ? {} : { defaultEffort: ReasoningEffortId(configured.defaultReasoningEffort) }
|
|
1200
|
-
} }
|
|
1201
|
-
});
|
|
1202
|
-
}
|
|
1203
|
-
stream(options) {
|
|
1204
|
-
return this.generate(options);
|
|
1205
|
-
}
|
|
1206
|
-
async *generate(options) {
|
|
1207
|
-
if (options.provider !== this.providerId) throw new QoderLlmError(`Qoder adapter does not own provider "${options.provider}".`, "INVALID_PROVIDER");
|
|
1208
|
-
if (options.signal?.aborted) throw new QoderLlmError("Request was aborted prior to generation.", "ABORTED");
|
|
1209
|
-
const model = this.catalogModels.find((candidate) => candidate.id === (options.model || "cmodel"));
|
|
1210
|
-
const messages = validateQoderRequest(options, model);
|
|
1211
|
-
const pat = await this.resolvePat();
|
|
1212
|
-
if (!pat) throw new QoderLlmError("Qoder Personal Access Token is missing. Configure Qoder in the Qoder settings page.", "MISSING_CREDENTIAL");
|
|
1213
|
-
if (options.signal?.aborted) throw new QoderLlmError("Request was aborted.", "ABORTED");
|
|
1214
|
-
const credentials = await this.authService.getCredentials(pat, options.signal, this.region);
|
|
1215
|
-
const request = buildQoderRequestBody(options, credentials.userID, messages, model);
|
|
1216
|
-
const encodedBody = qoderEncodeBody(JSON.stringify(request));
|
|
1217
|
-
const encodedBytes = Buffer.from(encodedBody, "utf8");
|
|
1218
|
-
const chatUrl = getQoderChatUrl(this.region);
|
|
1219
|
-
const requestController = new AbortController();
|
|
1220
|
-
let idleTimedOut = false;
|
|
1221
|
-
let idleTimer;
|
|
1222
|
-
const onCallerAbort = () => requestController.abort(options.signal?.reason);
|
|
1223
|
-
options.signal?.addEventListener("abort", onCallerAbort, { once: true });
|
|
1224
|
-
const resetIdleTimer = () => {
|
|
1225
|
-
if (idleTimer !== void 0) clearTimeout(idleTimer);
|
|
1226
|
-
idleTimer = setTimeout(() => {
|
|
1227
|
-
idleTimedOut = true;
|
|
1228
|
-
requestController.abort("stream idle timeout");
|
|
1229
|
-
}, this.streamIdleTimeoutMs);
|
|
1230
|
-
};
|
|
1231
|
-
resetIdleTimer();
|
|
1232
|
-
try {
|
|
1233
|
-
const response = await this.fetchImpl(chatUrl, {
|
|
1234
|
-
method: "POST",
|
|
1235
|
-
headers: {
|
|
1236
|
-
"content-type": "application/json",
|
|
1237
|
-
"accept": "text/event-stream",
|
|
1238
|
-
"cache-control": "no-cache",
|
|
1239
|
-
"accept-encoding": "identity",
|
|
1240
|
-
"x-model-key": options.model || "cmodel",
|
|
1241
|
-
"x-model-source": model?.source || "system",
|
|
1242
|
-
...attributionHeaders(),
|
|
1243
|
-
...buildAuthHeaders(encodedBytes, chatUrl, credentials)
|
|
1244
|
-
},
|
|
1245
|
-
body: encodedBytes,
|
|
1246
|
-
signal: requestController.signal
|
|
1247
|
-
});
|
|
1248
|
-
resetIdleTimer();
|
|
1249
|
-
if (!response.ok) throw qoderHttpError(`Qoder upstream service returned HTTP ${response.status}.`, response);
|
|
1250
|
-
if (!response.body) throw new QoderLlmError("Qoder response contains no readable body stream.", "EMPTY_RESPONSE");
|
|
1251
|
-
yield* parseQoderSse(response.body, { onActivity: resetIdleTimer });
|
|
1252
|
-
} catch (error) {
|
|
1253
|
-
if (options.signal?.aborted) throw new QoderLlmError("Request was aborted.", "ABORTED");
|
|
1254
|
-
if (idleTimedOut) {
|
|
1255
|
-
const failure = new QoderLlmError("Qoder model stream exceeded its idle timeout.", "TIMEOUT");
|
|
1256
|
-
this.logger?.error?.("[Qoder Stream] Request failed", redactLogValue(failure));
|
|
1257
|
-
throw failure;
|
|
1258
|
-
}
|
|
1259
|
-
if (error instanceof QoderLlmError) {
|
|
1260
|
-
this.logger?.error?.("[Qoder Stream] Request failed", redactLogValue(error));
|
|
1261
|
-
throw error;
|
|
1262
|
-
}
|
|
1263
|
-
const failure = new QoderLlmError("Qoder transport request failed.", "TRANSPORT", { cause: error });
|
|
1264
|
-
this.logger?.error?.("[Qoder Stream] Request failed", redactLogValue(failure), { cause: redactLogValue(error) });
|
|
1265
|
-
throw failure;
|
|
1266
|
-
} finally {
|
|
1267
|
-
if (idleTimer !== void 0) clearTimeout(idleTimer);
|
|
1268
|
-
options.signal?.removeEventListener("abort", onCallerAbort);
|
|
1269
|
-
requestController.abort("request complete");
|
|
1270
|
-
}
|
|
1271
|
-
}
|
|
1272
|
-
};
|
|
1273
|
-
//#endregion
|
|
1274
|
-
//#region src/credential-contract.ts
|
|
1275
|
-
/** Shared Host/browser contract for the one managed Qoder credential. */
|
|
1276
|
-
const qoderCredentialRef = "QODER_MANAGED_CREDENTIAL";
|
|
1277
|
-
/** DSH's local provider uses `env` and `*-env` for ambient credential layers. */
|
|
1278
|
-
function isEnvironmentCredentialSource(source) {
|
|
1279
|
-
return source === "env" || source?.endsWith("-env") === true;
|
|
1280
|
-
}
|
|
1281
|
-
//#endregion
|
|
1282
|
-
//#region src/credential.ts
|
|
1283
|
-
/** Host-side resolution of the one managed Qoder credential. */
|
|
1284
|
-
const managedQoderCredentialRef = credentialRef(qoderCredentialRef);
|
|
1285
|
-
/** Resolve only provider-managed storage; ambient environment layers are intentionally unsupported. */
|
|
1286
|
-
async function resolveManagedQoderPat(credentials) {
|
|
1287
|
-
const hit = await credentials.resolve(managedQoderCredentialRef);
|
|
1288
|
-
if (hit === void 0 || isEnvironmentCredentialSource(hit.source)) return "";
|
|
1289
|
-
return hit.value.trim();
|
|
1290
|
-
}
|
|
1291
|
-
//#endregion
|
|
1292
|
-
//#region src/models.ts
|
|
1293
|
-
const discoveredMetadataKeys = [
|
|
1294
|
-
"description",
|
|
1295
|
-
"source",
|
|
1296
|
-
"isReasoning",
|
|
1297
|
-
"supportsEffort",
|
|
1298
|
-
"reasoningEfforts",
|
|
1299
|
-
"defaultReasoningEffort",
|
|
1300
|
-
"priceFactor",
|
|
1301
|
-
"contextOptions"
|
|
1302
|
-
];
|
|
1303
|
-
function positiveNumber(value) {
|
|
1304
|
-
return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : void 0;
|
|
1305
|
-
}
|
|
1306
|
-
const reasoningEffortOrder = /* @__PURE__ */ new Map([
|
|
1307
|
-
["low", 0],
|
|
1308
|
-
["medium", 1],
|
|
1309
|
-
["high", 2],
|
|
1310
|
-
["xhigh", 3],
|
|
1311
|
-
["max", 4]
|
|
1312
|
-
]);
|
|
1313
|
-
function contextOptionsOf(value) {
|
|
1314
|
-
if (typeof value !== "object" || value === null || Array.isArray(value)) return void 0;
|
|
1315
|
-
const options = {};
|
|
1316
|
-
for (const [key, raw] of Object.entries(value)) {
|
|
1317
|
-
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) continue;
|
|
1318
|
-
const entry = raw;
|
|
1319
|
-
const tokenCount = positiveNumber(entry.token_count);
|
|
1320
|
-
const isDefault = typeof entry.is_default === "boolean" ? entry.is_default : void 0;
|
|
1321
|
-
if (tokenCount === void 0 && isDefault === void 0) continue;
|
|
1322
|
-
options[key] = {
|
|
1323
|
-
...tokenCount === void 0 ? {} : { tokenCount },
|
|
1324
|
-
...isDefault === void 0 ? {} : { isDefault }
|
|
1325
|
-
};
|
|
1326
|
-
}
|
|
1327
|
-
if (Object.keys(options).length === 0) return void 0;
|
|
1328
|
-
const largest = Math.max(...Object.values(options).map((option) => option.tokenCount ?? 0));
|
|
1329
|
-
if (largest <= 0) return options;
|
|
1330
|
-
return Object.fromEntries(Object.entries(options).map(([key, option]) => [key, {
|
|
1331
|
-
...option,
|
|
1332
|
-
isDefault: option.tokenCount === largest
|
|
1333
|
-
}]));
|
|
1334
|
-
}
|
|
1335
|
-
function reasoningEffortsOf(value) {
|
|
1336
|
-
if (typeof value !== "object" || value === null || Array.isArray(value)) return {};
|
|
1337
|
-
const enabled = value.enabled;
|
|
1338
|
-
if (typeof enabled !== "object" || enabled === null || Array.isArray(enabled)) return {};
|
|
1339
|
-
const rawEfforts = enabled.efforts;
|
|
1340
|
-
if (typeof rawEfforts !== "object" || rawEfforts === null || Array.isArray(rawEfforts)) return {};
|
|
1341
|
-
const efforts = [];
|
|
1342
|
-
let defaultEffort;
|
|
1343
|
-
for (const [id, raw] of Object.entries(rawEfforts)) {
|
|
1344
|
-
if (!id.trim() || typeof raw !== "object" || raw === null || Array.isArray(raw)) continue;
|
|
1345
|
-
const entry = raw;
|
|
1346
|
-
efforts.push({
|
|
1347
|
-
id,
|
|
1348
|
-
name: id,
|
|
1349
|
-
...typeof entry.description === "string" && entry.description.trim() ? { description: entry.description.trim() } : {}
|
|
1350
|
-
});
|
|
1351
|
-
if (entry.is_default === true) defaultEffort = id;
|
|
1352
|
-
}
|
|
1353
|
-
return {
|
|
1354
|
-
...efforts.length === 0 ? {} : { efforts: efforts.sort((left, right) => (reasoningEffortOrder.get(left.id) ?? Number.MAX_SAFE_INTEGER) - (reasoningEffortOrder.get(right.id) ?? Number.MAX_SAFE_INTEGER)) },
|
|
1355
|
-
...defaultEffort === void 0 ? {} : { defaultEffort }
|
|
1356
|
-
};
|
|
1357
|
-
}
|
|
1358
|
-
function normalizeQoderModels(payload) {
|
|
1359
|
-
if (typeof payload !== "object" || payload === null || !Array.isArray(payload.chat)) return [];
|
|
1360
|
-
const models = [];
|
|
1361
|
-
const seen = /* @__PURE__ */ new Set();
|
|
1362
|
-
for (const raw of payload.chat) {
|
|
1363
|
-
if (typeof raw !== "object" || raw === null || raw.enable !== true) continue;
|
|
1364
|
-
const id = typeof raw.key === "string" ? raw.key.trim() : "";
|
|
1365
|
-
if (!id || seen.has(id)) continue;
|
|
1366
|
-
seen.add(id);
|
|
1367
|
-
const contextOptions = contextOptionsOf(raw.context_config);
|
|
1368
|
-
const contextWindow = Math.max(positiveNumber(raw.max_input_tokens) ?? 18e4, ...Object.values(contextOptions ?? {}).map((option) => option.tokenCount ?? 0));
|
|
1369
|
-
const thinking = typeof raw.thinking_config === "object" && raw.thinking_config !== null;
|
|
1370
|
-
const reasoning = reasoningEffortsOf(raw.thinking_config);
|
|
1371
|
-
models.push({
|
|
1372
|
-
id,
|
|
1373
|
-
name: typeof raw.display_name === "string" && raw.display_name.trim() ? raw.display_name.trim() : id,
|
|
1374
|
-
contextWindow,
|
|
1375
|
-
maxTokens: positiveNumber(raw.max_output_tokens) ?? 32768,
|
|
1376
|
-
source: typeof raw.source === "string" && raw.source.trim() ? raw.source.trim() : "system",
|
|
1377
|
-
isReasoning: raw.is_reasoning === true || thinking,
|
|
1378
|
-
supportsEffort: reasoning.efforts !== void 0,
|
|
1379
|
-
...reasoning.efforts === void 0 ? {} : { reasoningEfforts: reasoning.efforts },
|
|
1380
|
-
...reasoning.defaultEffort === void 0 ? {} : { defaultReasoningEffort: reasoning.defaultEffort },
|
|
1381
|
-
...positiveNumber(raw.price_factor) === void 0 ? {} : { priceFactor: positiveNumber(raw.price_factor) },
|
|
1382
|
-
...contextOptions === void 0 ? {} : { contextOptions }
|
|
1383
|
-
});
|
|
2089
|
+
message: "Model returned a completed response with no content.",
|
|
2090
|
+
code: EMPTY_RESPONSE_CODE
|
|
2091
|
+
}
|
|
2092
|
+
}
|
|
2093
|
+
};
|
|
2094
|
+
return;
|
|
1384
2095
|
}
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
* has retained only id, name, and token limits.
|
|
1390
|
-
*/
|
|
1391
|
-
function mergeQoderDiscoveryMetadata(configured, discovered) {
|
|
1392
|
-
const catalog = new Map(discovered.map((model) => [model.id, model]));
|
|
1393
|
-
return configured.map((model) => {
|
|
1394
|
-
const advertised = catalog.get(model.id);
|
|
1395
|
-
if (advertised === void 0) return { ...model };
|
|
1396
|
-
const merged = { ...model };
|
|
1397
|
-
for (const key of discoveredMetadataKeys) delete merged[key];
|
|
1398
|
-
for (const key of discoveredMetadataKeys) if (advertised[key] !== void 0) Object.assign(merged, { [key]: advertised[key] });
|
|
1399
|
-
return merged;
|
|
1400
|
-
});
|
|
2096
|
+
yield {
|
|
2097
|
+
type: "finish",
|
|
2098
|
+
reason: { kind: toolCalls.size > 0 ? "tool-calls" : terminalKind }
|
|
2099
|
+
};
|
|
1401
2100
|
}
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
2101
|
+
//#endregion
|
|
2102
|
+
//#region src/qoder/transport/chat.ts
|
|
2103
|
+
/** Qoder chat request encoding, timeout lifecycle, and SSE streaming. */
|
|
2104
|
+
function aborted$1(message) {
|
|
2105
|
+
return new QoderLlmError(message, "ABORTED");
|
|
1407
2106
|
}
|
|
1408
|
-
async function
|
|
1409
|
-
const
|
|
1410
|
-
const
|
|
1411
|
-
|
|
1412
|
-
|
|
2107
|
+
async function* streamQoderChat(options, model, credentials, messages, dependencies) {
|
|
2108
|
+
const request = await buildQoderRequestBody(options, credentials.userID, messages, model);
|
|
2109
|
+
const encodedBody = qoderEncodeBody(JSON.stringify(request));
|
|
2110
|
+
const encodedBytes = Buffer.from(encodedBody, "utf8");
|
|
2111
|
+
const chatUrl = getQoderChatUrl(dependencies.region);
|
|
2112
|
+
const requestController = new AbortController();
|
|
2113
|
+
let headerTimedOut = false;
|
|
2114
|
+
let idleTimedOut = false;
|
|
2115
|
+
let idleTimer;
|
|
2116
|
+
const startedAt = performance.now();
|
|
2117
|
+
const onCallerAbort = () => requestController.abort(options.signal?.reason);
|
|
2118
|
+
options.signal?.addEventListener("abort", onCallerAbort, { once: true });
|
|
2119
|
+
const headerTimer = setTimeout(() => {
|
|
2120
|
+
headerTimedOut = true;
|
|
2121
|
+
requestController.abort("response header timeout");
|
|
2122
|
+
}, dependencies.responseHeaderTimeoutMs);
|
|
2123
|
+
const resetIdleTimer = () => {
|
|
2124
|
+
if (idleTimer !== void 0) clearTimeout(idleTimer);
|
|
2125
|
+
idleTimer = setTimeout(() => {
|
|
2126
|
+
idleTimedOut = true;
|
|
2127
|
+
requestController.abort("stream idle timeout");
|
|
2128
|
+
}, dependencies.streamIdleTimeoutMs);
|
|
2129
|
+
};
|
|
1413
2130
|
try {
|
|
1414
|
-
response = await
|
|
1415
|
-
method: "
|
|
2131
|
+
const response = await dependencies.fetch(chatUrl, {
|
|
2132
|
+
method: "POST",
|
|
1416
2133
|
headers: {
|
|
1417
|
-
|
|
1418
|
-
|
|
2134
|
+
"content-type": "application/json",
|
|
2135
|
+
"accept": "text/event-stream",
|
|
2136
|
+
"cache-control": "no-cache",
|
|
2137
|
+
"accept-encoding": "identity",
|
|
2138
|
+
"x-model-key": options.model || "cmodel",
|
|
2139
|
+
"x-model-source": model?.source || "system",
|
|
2140
|
+
...attributionHeaders(),
|
|
2141
|
+
...buildAuthHeaders(encodedBytes, chatUrl, credentials)
|
|
1419
2142
|
},
|
|
1420
|
-
|
|
2143
|
+
body: encodedBytes,
|
|
2144
|
+
signal: requestController.signal
|
|
2145
|
+
});
|
|
2146
|
+
clearTimeout(headerTimer);
|
|
2147
|
+
dependencies.logger?.debug?.("[Qoder Stream] Response headers received", {
|
|
2148
|
+
region: dependencies.region,
|
|
2149
|
+
status: response.status,
|
|
2150
|
+
durationMs: Math.round(performance.now() - startedAt),
|
|
2151
|
+
...qoderRequestId(response.headers) === void 0 ? {} : { requestId: qoderRequestId(response.headers) }
|
|
1421
2152
|
});
|
|
2153
|
+
resetIdleTimer();
|
|
2154
|
+
if (!response.ok) throw qoderHttpError(`Qoder upstream service returned HTTP ${response.status}.`, response);
|
|
2155
|
+
if (!response.body) throw new QoderLlmError("Qoder response contains no readable body stream.", "EMPTY_RESPONSE");
|
|
2156
|
+
yield* parseQoderSse(response.body, { onActivity: resetIdleTimer });
|
|
1422
2157
|
} catch (error) {
|
|
1423
|
-
if (options.signal?.aborted) throw
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
throw
|
|
2158
|
+
if (options.signal?.aborted) throw aborted$1("Request was aborted.");
|
|
2159
|
+
if (headerTimedOut) {
|
|
2160
|
+
const failure = new QoderLlmError("Qoder model request exceeded its response header timeout.", "TIMEOUT");
|
|
2161
|
+
dependencies.logger?.error?.("[Qoder Stream] Request failed", redactLogValue(failure));
|
|
2162
|
+
throw failure;
|
|
2163
|
+
}
|
|
2164
|
+
if (idleTimedOut) {
|
|
2165
|
+
const failure = new QoderLlmError("Qoder model stream exceeded its idle timeout.", "TIMEOUT");
|
|
2166
|
+
dependencies.logger?.error?.("[Qoder Stream] Request failed", redactLogValue(failure));
|
|
2167
|
+
throw failure;
|
|
2168
|
+
}
|
|
2169
|
+
if (error instanceof QoderLlmError) {
|
|
2170
|
+
dependencies.logger?.error?.("[Qoder Stream] Request failed", redactLogValue(error));
|
|
2171
|
+
throw error;
|
|
2172
|
+
}
|
|
2173
|
+
const failure = new QoderLlmError("Qoder transport request failed.", "TRANSPORT", { cause: error });
|
|
2174
|
+
dependencies.logger?.error?.("[Qoder Stream] Request failed", redactLogValue(failure), { cause: redactLogValue(error) });
|
|
2175
|
+
throw failure;
|
|
2176
|
+
} finally {
|
|
2177
|
+
clearTimeout(headerTimer);
|
|
2178
|
+
if (idleTimer !== void 0) clearTimeout(idleTimer);
|
|
2179
|
+
options.signal?.removeEventListener("abort", onCallerAbort);
|
|
2180
|
+
requestController.abort("request complete");
|
|
1441
2181
|
}
|
|
1442
|
-
const models = normalizeQoderModels(payload);
|
|
1443
|
-
if (models.length === 0) throw new QoderLlmError("Qoder model discovery returned no enabled models.", "EMPTY_RESPONSE");
|
|
1444
|
-
options.logger?.debug?.("[Qoder Models] Model catalog resolved", { models });
|
|
1445
|
-
return models;
|
|
1446
2182
|
}
|
|
1447
2183
|
//#endregion
|
|
1448
|
-
//#region src/
|
|
1449
|
-
const
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
function normalizeQuota(raw) {
|
|
1453
|
-
if (!raw || typeof raw !== "object") return void 0;
|
|
1454
|
-
const total = typeof raw.total === "number" && Number.isFinite(raw.total) ? raw.total : typeof raw.cap === "number" && Number.isFinite(raw.cap) ? raw.cap : typeof raw.remaining === "number" && typeof raw.used === "number" ? raw.used + raw.remaining : 0;
|
|
1455
|
-
const used = typeof raw.used === "number" && Number.isFinite(raw.used) ? raw.used : 0;
|
|
1456
|
-
const remaining = typeof raw.remaining === "number" && Number.isFinite(raw.remaining) ? raw.remaining : Math.max(0, total - used);
|
|
1457
|
-
let percentage;
|
|
1458
|
-
if (typeof raw.percentage === "number" && Number.isFinite(raw.percentage)) percentage = raw.percentage <= 1 && total > 1 ? raw.percentage * 100 : raw.percentage;
|
|
1459
|
-
else percentage = total > 0 ? used / total * 100 : 0;
|
|
1460
|
-
const unit = typeof raw.unit === "string" && raw.unit.length > 0 ? raw.unit : "credits";
|
|
1461
|
-
return {
|
|
1462
|
-
total,
|
|
1463
|
-
used,
|
|
1464
|
-
remaining,
|
|
1465
|
-
percentage,
|
|
1466
|
-
unit
|
|
1467
|
-
};
|
|
1468
|
-
}
|
|
1469
|
-
function normalizeExpiresAt(rawExpires) {
|
|
1470
|
-
if (rawExpires === void 0 || rawExpires === null) return void 0;
|
|
1471
|
-
if (typeof rawExpires === "number" && rawExpires > 0) return new Date(rawExpires).toISOString();
|
|
1472
|
-
if (typeof rawExpires === "string" && rawExpires.length > 0) {
|
|
1473
|
-
const parsed = Date.parse(rawExpires);
|
|
1474
|
-
if (!Number.isNaN(parsed) && parsed > 0) return new Date(parsed).toISOString();
|
|
1475
|
-
}
|
|
2184
|
+
//#region src/qoder/transport/default-transport.ts
|
|
2185
|
+
const defaultStreamIdleTimeoutMs = 300 * 1e3;
|
|
2186
|
+
function aborted(message) {
|
|
2187
|
+
return new QoderLlmError(message, "ABORTED");
|
|
1476
2188
|
}
|
|
1477
|
-
var
|
|
1478
|
-
authService;
|
|
1479
|
-
fetchImpl;
|
|
1480
|
-
ttlMs;
|
|
1481
|
-
timeoutMs;
|
|
1482
|
-
resolveRegion;
|
|
2189
|
+
var DefaultQoderTransport = class {
|
|
1483
2190
|
region;
|
|
2191
|
+
resolvePat;
|
|
2192
|
+
fetchImpl;
|
|
1484
2193
|
logger;
|
|
1485
|
-
|
|
2194
|
+
streamIdleTimeoutMs;
|
|
2195
|
+
responseHeaderTimeoutMs;
|
|
2196
|
+
metadataTimeoutMs;
|
|
2197
|
+
auth;
|
|
2198
|
+
usage;
|
|
2199
|
+
modelFlights = new SingleFlight();
|
|
2200
|
+
attachments;
|
|
2201
|
+
imageUploader;
|
|
1486
2202
|
constructor(options) {
|
|
1487
|
-
this.authService = options.authService;
|
|
1488
|
-
this.fetchImpl = options.fetch ?? globalThis.fetch;
|
|
1489
|
-
this.ttlMs = options.ttlMs ?? defaultUsageTtlMs;
|
|
1490
|
-
this.timeoutMs = options.timeoutMs ?? defaultUsageTimeoutMs;
|
|
1491
|
-
this.resolveRegion = options.resolveRegion;
|
|
1492
2203
|
this.region = options.region;
|
|
2204
|
+
this.resolvePat = options.resolvePat;
|
|
2205
|
+
this.fetchImpl = options.fetch ?? globalThis.fetch;
|
|
1493
2206
|
this.logger = options.logger;
|
|
2207
|
+
this.streamIdleTimeoutMs = options.streamIdleTimeoutMs ?? 3e5;
|
|
2208
|
+
this.responseHeaderTimeoutMs = options.responseHeaderTimeoutMs ?? 3e4;
|
|
2209
|
+
this.metadataTimeoutMs = options.metadataTimeoutMs;
|
|
2210
|
+
this.attachments = options.attachments;
|
|
2211
|
+
this.auth = new QoderAuthService({
|
|
2212
|
+
fetch: this.fetchImpl,
|
|
2213
|
+
logger: this.logger,
|
|
2214
|
+
region: this.region,
|
|
2215
|
+
resolveMachineId: options.resolveMachineId
|
|
2216
|
+
});
|
|
2217
|
+
this.usage = new QoderUsageReader({
|
|
2218
|
+
authService: this.auth,
|
|
2219
|
+
fetch: this.fetchImpl,
|
|
2220
|
+
logger: this.logger,
|
|
2221
|
+
region: this.region,
|
|
2222
|
+
timeoutMs: this.metadataTimeoutMs
|
|
2223
|
+
});
|
|
2224
|
+
this.imageUploader = new QoderImageUploader({
|
|
2225
|
+
fetch: this.fetchImpl,
|
|
2226
|
+
logger: this.logger,
|
|
2227
|
+
region: this.region,
|
|
2228
|
+
...options.imageUploadTimeoutMs === void 0 ? {} : { timeoutMs: options.imageUploadTimeoutMs },
|
|
2229
|
+
...options.imageUrlCacheTtlMs === void 0 ? {} : { cacheTtlMs: options.imageUrlCacheTtlMs },
|
|
2230
|
+
refreshCredentials: async (signal) => {
|
|
2231
|
+
const pat = await this.requirePat(signal);
|
|
2232
|
+
this.auth.clear(pat);
|
|
2233
|
+
return this.auth.getCredentials(pat, signal);
|
|
2234
|
+
}
|
|
2235
|
+
});
|
|
1494
2236
|
}
|
|
1495
|
-
|
|
1496
|
-
this.
|
|
2237
|
+
stream(options, model) {
|
|
2238
|
+
return this.generate(options, model);
|
|
1497
2239
|
}
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
2240
|
+
async discoverModels(signal) {
|
|
2241
|
+
const pat = await this.requirePat(signal);
|
|
2242
|
+
const key = opaqueCredentialKey(pat);
|
|
2243
|
+
return this.modelFlights.run(key, signal, async (sharedSignal) => {
|
|
2244
|
+
const credentials = await this.auth.getCredentials(pat, sharedSignal);
|
|
2245
|
+
return retryMetadataRead(sharedSignal, () => fetchQoderModels(credentials, {
|
|
2246
|
+
fetch: this.fetchImpl,
|
|
2247
|
+
signal: sharedSignal,
|
|
2248
|
+
logger: this.logger,
|
|
2249
|
+
region: this.region,
|
|
2250
|
+
timeoutMs: this.metadataTimeoutMs
|
|
2251
|
+
}));
|
|
2252
|
+
}, () => aborted("Qoder model discovery was aborted."));
|
|
1501
2253
|
}
|
|
1502
|
-
async readAccount(
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
const cached = this.cache.get(cacheKey);
|
|
1508
|
-
if (cached && cached.expiresAt > Date.now()) return cached.info;
|
|
1509
|
-
}
|
|
1510
|
-
const creds = await this.authService.getCredentials(pat, options?.signal, effectiveRegion);
|
|
1511
|
-
const accountInfo = {
|
|
1512
|
-
profile: {
|
|
1513
|
-
id: creds.userID,
|
|
1514
|
-
name: creds.name || "Qoder User",
|
|
1515
|
-
email: creds.email || ""
|
|
1516
|
-
},
|
|
1517
|
-
usage: await this.fetchUsage(creds.authToken, options?.signal, effectiveRegion),
|
|
1518
|
-
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1519
|
-
};
|
|
1520
|
-
this.cache.set(cacheKey, {
|
|
1521
|
-
info: accountInfo,
|
|
1522
|
-
expiresAt: Date.now() + this.ttlMs
|
|
2254
|
+
async readAccount(options) {
|
|
2255
|
+
const pat = await this.requirePat(options?.signal);
|
|
2256
|
+
return this.usage.readAccount(pat, {
|
|
2257
|
+
force: options?.force,
|
|
2258
|
+
signal: options?.signal
|
|
1523
2259
|
});
|
|
1524
|
-
return accountInfo;
|
|
1525
2260
|
}
|
|
1526
|
-
|
|
1527
|
-
if (
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
status: response.status,
|
|
1550
|
-
statusText: response.statusText
|
|
1551
|
-
});
|
|
1552
|
-
const text = await response.text();
|
|
1553
|
-
if (!response.ok) {
|
|
1554
|
-
this.logger?.error?.("[Qoder Usage] Request failed", redactLogPayload(text));
|
|
1555
|
-
throw qoderHttpError(`Failed to fetch Qoder quota usage with status ${response.status}: ${text}`, response);
|
|
1556
|
-
}
|
|
1557
|
-
let data;
|
|
1558
|
-
try {
|
|
1559
|
-
data = JSON.parse(text);
|
|
1560
|
-
} catch {
|
|
1561
|
-
this.logger?.error?.("[Qoder Usage] Invalid JSON response", redactLogPayload(text));
|
|
1562
|
-
throw new QoderLlmError("Failed to parse Qoder quota JSON response", "USAGE_FETCH_FAILED");
|
|
1563
|
-
}
|
|
1564
|
-
this.logger?.debug?.("[Qoder Usage] Quota usage resolved", redactLogValue(data));
|
|
1565
|
-
return {
|
|
1566
|
-
userQuota: normalizeQuota(data.userQuota),
|
|
1567
|
-
orgResourcePackage: normalizeQuota(data.orgResourcePackage),
|
|
1568
|
-
totalUsagePercentage: typeof data.totalUsagePercentage === "number" ? data.totalUsagePercentage : void 0,
|
|
1569
|
-
isQuotaExceeded: typeof data.isQuotaExceeded === "boolean" ? data.isQuotaExceeded : false,
|
|
1570
|
-
expiresAt: normalizeExpiresAt(data.expiresAt),
|
|
1571
|
-
raw: data
|
|
1572
|
-
};
|
|
1573
|
-
} catch (error) {
|
|
1574
|
-
if (error instanceof QoderLlmError) throw error;
|
|
1575
|
-
if (signal?.aborted) throw new QoderLlmError("Qoder quota usage request was aborted.", "ABORTED");
|
|
1576
|
-
if (timeoutSignal.aborted) throw new QoderLlmError("Qoder quota usage request timed out.", "TIMEOUT");
|
|
1577
|
-
throw new QoderLlmError("Qoder quota usage network request failed.", "TRANSPORT", { cause: error });
|
|
1578
|
-
}
|
|
2261
|
+
async requirePat(signal) {
|
|
2262
|
+
if (signal?.aborted) throw aborted("Qoder request was aborted.");
|
|
2263
|
+
const pat = (await this.resolvePat()).trim();
|
|
2264
|
+
if (!pat) throw new QoderLlmError("Qoder Personal Access Token is missing. Configure Qoder in the Qoder settings page.", "MISSING_CREDENTIAL");
|
|
2265
|
+
if (signal?.aborted) throw aborted("Qoder request was aborted.");
|
|
2266
|
+
return pat;
|
|
2267
|
+
}
|
|
2268
|
+
async *generate(options, model) {
|
|
2269
|
+
if (options.signal?.aborted) throw aborted("Request was aborted prior to generation.");
|
|
2270
|
+
validateQoderRequestShape(options, model);
|
|
2271
|
+
const pat = await this.requirePat(options.signal);
|
|
2272
|
+
const credentials = await this.auth.getCredentials(pat, options.signal);
|
|
2273
|
+
const messages = await translateQoderMessages(options, this.attachments, {
|
|
2274
|
+
uploader: this.imageUploader,
|
|
2275
|
+
credentials
|
|
2276
|
+
});
|
|
2277
|
+
yield* streamQoderChat(options, model, await this.auth.getCredentials(pat, options.signal), messages, {
|
|
2278
|
+
fetch: this.fetchImpl,
|
|
2279
|
+
logger: this.logger,
|
|
2280
|
+
region: this.region,
|
|
2281
|
+
responseHeaderTimeoutMs: this.responseHeaderTimeoutMs,
|
|
2282
|
+
streamIdleTimeoutMs: this.streamIdleTimeoutMs
|
|
2283
|
+
});
|
|
1579
2284
|
}
|
|
1580
2285
|
};
|
|
1581
2286
|
//#endregion
|
|
1582
|
-
//#region src/index.ts
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
const providerQoder = "qoder-official";
|
|
1590
|
-
const settingsNamespace = "provider-qoder";
|
|
1591
|
-
const qoderChannel = "/qoder-subscription";
|
|
1592
|
-
const fiberDisposed = 4;
|
|
1593
|
-
const fiberUnloading = 5;
|
|
2287
|
+
//#region src/qoder/transport/index.ts
|
|
2288
|
+
function createQoderTransport(options) {
|
|
2289
|
+
return new DefaultQoderTransport(options);
|
|
2290
|
+
}
|
|
2291
|
+
//#endregion
|
|
2292
|
+
//#region src/dsh/config.ts
|
|
2293
|
+
/** DSH configuration schema and region-scoped model catalog migration rules. */
|
|
1594
2294
|
const catalogModel = z.object({
|
|
1595
2295
|
id: z.string().required(),
|
|
1596
2296
|
name: z.string().required(),
|
|
1597
2297
|
description: z.string(),
|
|
1598
2298
|
contextWindow: z.number().step(1).min(1),
|
|
2299
|
+
maxContextWindow: z.number().step(1).min(1),
|
|
1599
2300
|
maxTokens: z.number().step(1).min(1),
|
|
1600
2301
|
source: z.string(),
|
|
1601
2302
|
isReasoning: z.boolean(),
|
|
@@ -1607,14 +2308,17 @@ const catalogModel = z.object({
|
|
|
1607
2308
|
})),
|
|
1608
2309
|
defaultReasoningEffort: z.string(),
|
|
1609
2310
|
priceFactor: z.number().min(0),
|
|
2311
|
+
supportsImages: z.boolean(),
|
|
1610
2312
|
contextOptions: z.dict(z.object({
|
|
1611
2313
|
tokenCount: z.number().step(1).min(1),
|
|
1612
2314
|
isDefault: z.boolean()
|
|
1613
2315
|
}))
|
|
1614
2316
|
});
|
|
2317
|
+
const modelsByRegionSchema = z.dict(z.array(catalogModel));
|
|
1615
2318
|
const Config = z.object({
|
|
1616
2319
|
region: z.union(["global", "china"]).default("global"),
|
|
1617
|
-
|
|
2320
|
+
modelsByRegion: modelsByRegionSchema.default({}),
|
|
2321
|
+
models: z.array(catalogModel),
|
|
1618
2322
|
streamIdleTimeoutMs: z.number().step(1).min(1).default(defaultStreamIdleTimeoutMs)
|
|
1619
2323
|
});
|
|
1620
2324
|
function resolveModels(models) {
|
|
@@ -1628,6 +2332,117 @@ function resolveModels(models) {
|
|
|
1628
2332
|
return { ...model };
|
|
1629
2333
|
});
|
|
1630
2334
|
}
|
|
2335
|
+
function modelsFor(config, region, legacyModelsRegion = config.region ?? "global") {
|
|
2336
|
+
const scoped = config.modelsByRegion?.[region];
|
|
2337
|
+
if (scoped !== void 0) return resolveModels(scoped);
|
|
2338
|
+
if (legacyModelsRegion === region && config.models !== void 0 && config.models.length > 0) return resolveModels(config.models);
|
|
2339
|
+
return resolveModels(defaultModels);
|
|
2340
|
+
}
|
|
2341
|
+
//#endregion
|
|
2342
|
+
//#region src/dsh/adapter.ts
|
|
2343
|
+
/** Thin DSH adapter over the Qoder transport seam. */
|
|
2344
|
+
function modelInfo(provider, model) {
|
|
2345
|
+
return {
|
|
2346
|
+
provider,
|
|
2347
|
+
id: model.id,
|
|
2348
|
+
name: model.priceFactor === void 0 ? model.name : `${model.name} (${model.priceFactor}x)`,
|
|
2349
|
+
description: model.description,
|
|
2350
|
+
inputModalities: model.supportsImages === true ? ["text", "image"] : ["text"]
|
|
2351
|
+
};
|
|
2352
|
+
}
|
|
2353
|
+
var QoderAdapter = class extends LlmAdapter {
|
|
2354
|
+
resolveTransport;
|
|
2355
|
+
catalogModels;
|
|
2356
|
+
providerId;
|
|
2357
|
+
providerName;
|
|
2358
|
+
constructor(options) {
|
|
2359
|
+
super();
|
|
2360
|
+
this.resolveTransport = options.resolveTransport;
|
|
2361
|
+
this.catalogModels = options.models && options.models.length > 0 ? options.models : defaultModels;
|
|
2362
|
+
this.providerId = options.providerId ?? "qoder-official";
|
|
2363
|
+
this.providerName = options.providerName ?? "Qoder";
|
|
2364
|
+
}
|
|
2365
|
+
providerInfo(provider) {
|
|
2366
|
+
return {
|
|
2367
|
+
id: provider,
|
|
2368
|
+
name: this.providerName
|
|
2369
|
+
};
|
|
2370
|
+
}
|
|
2371
|
+
listModels(provider) {
|
|
2372
|
+
return Promise.resolve(this.catalogModels.map((model) => modelInfo(provider, model)));
|
|
2373
|
+
}
|
|
2374
|
+
replaceModels(models) {
|
|
2375
|
+
this.catalogModels = models;
|
|
2376
|
+
}
|
|
2377
|
+
resolveModel(provider, modelId, signal) {
|
|
2378
|
+
if (signal?.aborted) return Promise.reject(new QoderLlmError("Qoder model resolution was aborted.", "ABORTED"));
|
|
2379
|
+
const configured = this.catalogModels.find((model) => model.id === modelId);
|
|
2380
|
+
if (configured === void 0) return Promise.resolve({
|
|
2381
|
+
provider,
|
|
2382
|
+
id: modelId,
|
|
2383
|
+
name: modelId,
|
|
2384
|
+
inputModalities: ["text"]
|
|
2385
|
+
});
|
|
2386
|
+
return Promise.resolve({
|
|
2387
|
+
...modelInfo(provider, configured),
|
|
2388
|
+
...configured.contextWindow === void 0 ? {} : { context: { contextWindow: configured.contextWindow } },
|
|
2389
|
+
...configured.maxTokens === void 0 ? {} : { defaultMaxTokens: configured.maxTokens },
|
|
2390
|
+
...configured.reasoningEfforts === void 0 || configured.reasoningEfforts.length === 0 ? {} : { reasoning: {
|
|
2391
|
+
efforts: configured.reasoningEfforts.map((effort) => ({
|
|
2392
|
+
id: ReasoningEffortId(effort.id),
|
|
2393
|
+
name: effort.name,
|
|
2394
|
+
...effort.description === void 0 ? {} : { description: effort.description }
|
|
2395
|
+
})),
|
|
2396
|
+
...configured.isReasoning === false || configured.defaultReasoningEffort === void 0 ? {} : { defaultEffort: ReasoningEffortId(configured.defaultReasoningEffort) }
|
|
2397
|
+
} }
|
|
2398
|
+
});
|
|
2399
|
+
}
|
|
2400
|
+
stream(options) {
|
|
2401
|
+
if (options.provider !== this.providerId) throw new QoderLlmError(`Qoder adapter does not own provider "${options.provider}".`, "INVALID_PROVIDER");
|
|
2402
|
+
const model = this.catalogModels.find((candidate) => candidate.id === (options.model || "cmodel"));
|
|
2403
|
+
return this.resolveTransport().stream(options, model);
|
|
2404
|
+
}
|
|
2405
|
+
};
|
|
2406
|
+
//#endregion
|
|
2407
|
+
//#region src/dsh/credential-contract.ts
|
|
2408
|
+
/** Shared DSH host/browser contract for the one managed Qoder credential. */
|
|
2409
|
+
const qoderCredentialRef = "QODER_MANAGED_CREDENTIAL";
|
|
2410
|
+
/** DSH's local provider uses `env` and `*-env` for ambient credential layers. */
|
|
2411
|
+
function isEnvironmentCredentialSource(source) {
|
|
2412
|
+
return source === "env" || source?.endsWith("-env") === true;
|
|
2413
|
+
}
|
|
2414
|
+
//#endregion
|
|
2415
|
+
//#region src/dsh/credential.ts
|
|
2416
|
+
/** DSH host-side resolution of the one managed Qoder credential. */
|
|
2417
|
+
const managedQoderCredentialRef = credentialRef(qoderCredentialRef);
|
|
2418
|
+
/** Resolve only provider-managed storage; ambient environment layers are intentionally unsupported. */
|
|
2419
|
+
async function resolveManagedQoderPat(credentials) {
|
|
2420
|
+
const hit = await credentials.resolve(managedQoderCredentialRef);
|
|
2421
|
+
if (hit === void 0 || isEnvironmentCredentialSource(hit.source)) return "";
|
|
2422
|
+
return hit.value.trim();
|
|
2423
|
+
}
|
|
2424
|
+
//#endregion
|
|
2425
|
+
//#region src/dsh/plugin.ts
|
|
2426
|
+
const name = "provider-qoder";
|
|
2427
|
+
const inject = [
|
|
2428
|
+
"llm",
|
|
2429
|
+
"credentials",
|
|
2430
|
+
"connection",
|
|
2431
|
+
"attachments"
|
|
2432
|
+
];
|
|
2433
|
+
const providerQoder = "qoder-official";
|
|
2434
|
+
const settingsNamespace = "provider-qoder";
|
|
2435
|
+
const qoderChannel = "/qoder-subscription";
|
|
2436
|
+
const fiberDisposed = 4;
|
|
2437
|
+
const fiberUnloading = 5;
|
|
2438
|
+
function logError(error) {
|
|
2439
|
+
if (!(error instanceof Error)) return { name: "UnknownError" };
|
|
2440
|
+
return {
|
|
2441
|
+
name: error.name,
|
|
2442
|
+
message: error.message,
|
|
2443
|
+
..."code" in error ? { code: error.code } : {}
|
|
2444
|
+
};
|
|
2445
|
+
}
|
|
1631
2446
|
function publicError(message) {
|
|
1632
2447
|
return {
|
|
1633
2448
|
ok: false,
|
|
@@ -1640,81 +2455,114 @@ function publicError(message) {
|
|
|
1640
2455
|
}
|
|
1641
2456
|
function apply(ctx, config = {}) {
|
|
1642
2457
|
const logger = ctx.logger;
|
|
1643
|
-
|
|
1644
|
-
const
|
|
1645
|
-
logger,
|
|
1646
|
-
resolveRegion: () => current().region ?? "global"
|
|
1647
|
-
});
|
|
1648
|
-
const usageReader = new QoderUsageReader({
|
|
1649
|
-
authService,
|
|
1650
|
-
logger,
|
|
1651
|
-
resolveRegion: () => current().region ?? "global"
|
|
1652
|
-
});
|
|
2458
|
+
const initialRegion = config.region ?? "global";
|
|
2459
|
+
const hasConfiguredModels = config.models !== void 0 && config.models.length > 0;
|
|
1653
2460
|
const baseConfig = {
|
|
1654
|
-
region:
|
|
1655
|
-
|
|
2461
|
+
region: initialRegion,
|
|
2462
|
+
modelsByRegion: { ...config.modelsByRegion },
|
|
2463
|
+
...hasConfiguredModels ? { models: resolveModels(config.models) } : {},
|
|
1656
2464
|
streamIdleTimeoutMs: config.streamIdleTimeoutMs ?? 3e5
|
|
1657
2465
|
};
|
|
1658
|
-
let
|
|
1659
|
-
let
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
2466
|
+
let current = () => baseConfig;
|
|
2467
|
+
let legacyModelsRegion = initialRegion;
|
|
2468
|
+
let persistDiscoveredModels = async (_region) => {};
|
|
2469
|
+
const discoveredCatalogs = {
|
|
2470
|
+
global: [],
|
|
2471
|
+
china: []
|
|
2472
|
+
};
|
|
2473
|
+
const createTransport = (region, streamIdleTimeoutMs, resolvePat = () => resolveManagedQoderPat(ctx.credentials)) => createQoderTransport({
|
|
2474
|
+
region,
|
|
2475
|
+
resolvePat,
|
|
2476
|
+
logger,
|
|
2477
|
+
streamIdleTimeoutMs,
|
|
2478
|
+
attachments: ctx.attachments
|
|
1664
2479
|
});
|
|
2480
|
+
const resolveConfig = () => {
|
|
2481
|
+
const value = current();
|
|
2482
|
+
const region = value.region ?? "global";
|
|
2483
|
+
return {
|
|
2484
|
+
region,
|
|
2485
|
+
models: mergeQoderDiscoveryMetadata(modelsFor(value, region, legacyModelsRegion), discoveredCatalogs[region]),
|
|
2486
|
+
streamIdleTimeoutMs: value.streamIdleTimeoutMs ?? 3e5
|
|
2487
|
+
};
|
|
2488
|
+
};
|
|
1665
2489
|
const initial = resolveConfig();
|
|
2490
|
+
let activeTransport = createTransport(initial.region, initial.streamIdleTimeoutMs);
|
|
2491
|
+
let activeTransportConfig = {
|
|
2492
|
+
region: initial.region,
|
|
2493
|
+
streamIdleTimeoutMs: initial.streamIdleTimeoutMs
|
|
2494
|
+
};
|
|
1666
2495
|
const adapter = new QoderAdapter({
|
|
1667
|
-
|
|
2496
|
+
resolveTransport: () => activeTransport,
|
|
1668
2497
|
models: initial.models,
|
|
1669
2498
|
providerId: providerQoder,
|
|
1670
|
-
providerName: "Qoder"
|
|
1671
|
-
authService,
|
|
1672
|
-
logger,
|
|
1673
|
-
streamIdleTimeoutMs: initial.streamIdleTimeoutMs,
|
|
1674
|
-
region: initial.region
|
|
2499
|
+
providerName: "Qoder"
|
|
1675
2500
|
});
|
|
1676
2501
|
const registration = ctx.llm.registerAdapter([providerQoder], adapter);
|
|
1677
2502
|
const refreshAdapter = () => {
|
|
1678
2503
|
const next = resolveConfig();
|
|
1679
|
-
if (next.region !==
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
2504
|
+
if (next.region !== activeTransportConfig.region || next.streamIdleTimeoutMs !== activeTransportConfig.streamIdleTimeoutMs) {
|
|
2505
|
+
activeTransport = createTransport(next.region, next.streamIdleTimeoutMs);
|
|
2506
|
+
activeTransportConfig = {
|
|
2507
|
+
region: next.region,
|
|
2508
|
+
streamIdleTimeoutMs: next.streamIdleTimeoutMs
|
|
2509
|
+
};
|
|
1683
2510
|
}
|
|
1684
|
-
adapter.
|
|
2511
|
+
adapter.replaceModels(next.models);
|
|
1685
2512
|
registration.replace([providerQoder]);
|
|
1686
2513
|
};
|
|
1687
2514
|
ctx.inject(["settings"], (settingsCtx) => {
|
|
1688
2515
|
const scope = settingsCtx.settings.register(settingsNamespace, Config, {
|
|
1689
2516
|
base: baseConfig,
|
|
1690
2517
|
validate: (value) => {
|
|
1691
|
-
|
|
2518
|
+
for (const region of Object.keys(value.modelsByRegion ?? {})) if (region !== "global" && region !== "china") throw new Error(`provider-qoder: unsupported model catalog region "${region}"`);
|
|
2519
|
+
modelsFor(value, "global", legacyModelsRegion);
|
|
2520
|
+
modelsFor(value, "china", legacyModelsRegion);
|
|
1692
2521
|
}
|
|
1693
2522
|
});
|
|
2523
|
+
legacyModelsRegion = scope.get().region ?? initialRegion;
|
|
1694
2524
|
current = () => scope.get();
|
|
2525
|
+
persistDiscoveredModels = async (region) => {
|
|
2526
|
+
const value = scope.get();
|
|
2527
|
+
const selected = modelsFor(value, region, legacyModelsRegion);
|
|
2528
|
+
const enriched = mergeQoderDiscoveryMetadata(selected, discoveredCatalogs[region]);
|
|
2529
|
+
if (!hasSameQoderDiscoveryMetadata(selected, enriched)) await scope.update({ modelsByRegion: {
|
|
2530
|
+
...value.modelsByRegion,
|
|
2531
|
+
[region]: enriched
|
|
2532
|
+
} });
|
|
2533
|
+
};
|
|
1695
2534
|
refreshAdapter();
|
|
2535
|
+
const loaded = scope.get();
|
|
2536
|
+
const loadedRegion = loaded.region ?? "global";
|
|
2537
|
+
if (loaded.models !== void 0 && loaded.models.length > 0 && loaded.modelsByRegion?.[loadedRegion] === void 0) scope.update({ modelsByRegion: {
|
|
2538
|
+
...loaded.modelsByRegion,
|
|
2539
|
+
[loadedRegion]: resolveModels(loaded.models)
|
|
2540
|
+
} }).catch((error) => logger?.error?.("[Qoder Settings] Failed to migrate model catalog", logError(error)));
|
|
1696
2541
|
scope.watch(async (next) => {
|
|
1697
2542
|
if (ctx.fiber.state === fiberUnloading || ctx.fiber.state === fiberDisposed) return;
|
|
1698
2543
|
refreshAdapter();
|
|
1699
|
-
const
|
|
1700
|
-
|
|
2544
|
+
const region = next.region ?? "global";
|
|
2545
|
+
const selected = modelsFor(next, region, legacyModelsRegion);
|
|
2546
|
+
const enriched = mergeQoderDiscoveryMetadata(selected, discoveredCatalogs[region]);
|
|
2547
|
+
if (!hasSameQoderDiscoveryMetadata(selected, enriched)) await scope.update({ modelsByRegion: {
|
|
2548
|
+
...next.modelsByRegion,
|
|
2549
|
+
[region]: enriched
|
|
2550
|
+
} });
|
|
1701
2551
|
});
|
|
1702
2552
|
settingsCtx.effect(() => () => {
|
|
1703
2553
|
if (ctx.fiber.state === fiberUnloading || ctx.fiber.state === fiberDisposed) return;
|
|
2554
|
+
persistDiscoveredModels = async () => {};
|
|
1704
2555
|
current = () => baseConfig;
|
|
1705
2556
|
refreshAdapter();
|
|
1706
2557
|
});
|
|
1707
2558
|
});
|
|
1708
2559
|
const discoverModels = async (signal, suppliedPat) => {
|
|
1709
|
-
const
|
|
1710
|
-
|
|
1711
|
-
const
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
region
|
|
1716
|
-
});
|
|
1717
|
-
discoveredCatalog = models;
|
|
2560
|
+
const snapshot = resolveConfig();
|
|
2561
|
+
const normalizedPat = suppliedPat?.trim();
|
|
2562
|
+
const models = await (normalizedPat ? createTransport(snapshot.region, snapshot.streamIdleTimeoutMs, () => Promise.resolve(normalizedPat)) : activeTransport).discoverModels(signal);
|
|
2563
|
+
discoveredCatalogs[snapshot.region] = models;
|
|
2564
|
+
refreshAdapter();
|
|
2565
|
+
await persistDiscoveredModels(snapshot.region);
|
|
1718
2566
|
return models;
|
|
1719
2567
|
};
|
|
1720
2568
|
ctx.llm.registerModelDiscovery(settingsNamespace, async (request, signal) => {
|
|
@@ -1737,28 +2585,15 @@ function apply(ctx, config = {}) {
|
|
|
1737
2585
|
};
|
|
1738
2586
|
} catch (error) {
|
|
1739
2587
|
if (signal.aborted || error instanceof QoderLlmError && error.code === "ABORTED") throw error;
|
|
1740
|
-
logger?.error?.("[Qoder RPC] Failed to discover models",
|
|
2588
|
+
logger?.error?.("[Qoder RPC] Failed to discover models", logError(error));
|
|
1741
2589
|
return publicError(error instanceof Error ? error.message : "Failed to discover Qoder models");
|
|
1742
2590
|
}
|
|
1743
2591
|
const force = typeof payload === "object" && payload !== null && "force" in payload ? payload.force === true : false;
|
|
1744
2592
|
logger?.debug?.("[Qoder RPC] Reading subscriber account", { force });
|
|
1745
|
-
let pat;
|
|
1746
|
-
try {
|
|
1747
|
-
pat = await resolveManagedQoderPat(ctx.credentials);
|
|
1748
|
-
} catch (error) {
|
|
1749
|
-
if (signal.aborted) throw error;
|
|
1750
|
-
logger?.error?.("[Qoder RPC] Failed to resolve managed PAT", redactLogValue(error));
|
|
1751
|
-
return publicError(error instanceof Error ? error.message : "No Qoder PAT configured");
|
|
1752
|
-
}
|
|
1753
|
-
if (!pat) {
|
|
1754
|
-
logger?.warn?.("[Qoder RPC] No managed Qoder PAT is configured");
|
|
1755
|
-
return publicError("Qoder PAT is not configured");
|
|
1756
|
-
}
|
|
1757
2593
|
try {
|
|
1758
|
-
const account = await
|
|
2594
|
+
const account = await activeTransport.readAccount({
|
|
1759
2595
|
force,
|
|
1760
|
-
signal
|
|
1761
|
-
region: current().region ?? "global"
|
|
2596
|
+
signal
|
|
1762
2597
|
});
|
|
1763
2598
|
logger?.debug?.("[Qoder RPC] Subscriber account resolved");
|
|
1764
2599
|
return {
|
|
@@ -1767,7 +2602,7 @@ function apply(ctx, config = {}) {
|
|
|
1767
2602
|
};
|
|
1768
2603
|
} catch (error) {
|
|
1769
2604
|
if (signal.aborted || error instanceof QoderLlmError && error.code === "ABORTED") throw error;
|
|
1770
|
-
logger?.error?.("[Qoder RPC] Failed to read subscriber account",
|
|
2605
|
+
logger?.error?.("[Qoder RPC] Failed to read subscriber account", logError(error));
|
|
1771
2606
|
return publicError(error instanceof Error ? error.message : "Failed to load Qoder account");
|
|
1772
2607
|
}
|
|
1773
2608
|
};
|
|
@@ -1775,4 +2610,4 @@ function apply(ctx, config = {}) {
|
|
|
1775
2610
|
}
|
|
1776
2611
|
}
|
|
1777
2612
|
//#endregion
|
|
1778
|
-
export { Config,
|
|
2613
|
+
export { Config, apply, inject, name };
|